Refactor Printer

It's now easier to customize Printer if in different situations objects should
be printed differently. Also its API is cleaner now. Names of methods of SkylarkValue objects now reflect names of Skylark functions: SkylarkValue#repr and SkylarkPrintableValue#str.

PiperOrigin-RevId: 160635154
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java b/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
index c67f658..b410ed8 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BaseFunction.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.Ordering;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
@@ -567,7 +568,7 @@
   }
 
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.append(buffer, "<function " + getName() + ">");
+  public void repr(SkylarkPrinter printer) {
+    printer.append("<function " + getName() + ">");
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
index ba86818..fdb7f54 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/BinaryOperatorExpression.java
@@ -20,7 +20,6 @@
 import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.IllegalFormatException;
 
 /**
@@ -336,7 +335,8 @@
   }
 
   /** Implements Operator.PERCENT. */
-  private static Object percent(Object lval, Object rval, Location location) throws EvalException {
+  private static Object percent(Object lval, Object rval, Location location)
+      throws EvalException {
     // int % int
     if (lval instanceof Integer && rval instanceof Integer) {
       if (rval.equals(0)) {
@@ -359,9 +359,9 @@
       String pattern = (String) lval;
       try {
         if (rval instanceof Tuple) {
-          return Printer.formatToString(pattern, (Tuple) rval);
+          return Printer.formatWithList(pattern, (Tuple) rval);
         }
-        return Printer.formatToString(pattern, Collections.singletonList(rval));
+        return Printer.format(pattern, rval);
       } catch (IllegalFormatException e) {
         throw new EvalException(location, e.getMessage());
       }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
index 45abea5..1a8d2b3 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Environment.java
@@ -698,8 +698,8 @@
       // End users don't have access to setupDynamic, and it is an implementation error
       // if we encounter a mutability exception.
       throw new AssertionError(
-          Printer.format(
-              "Trying to bind dynamic variable '%s' in frozen environment %r", varname, this),
+          String.format(
+              "Trying to bind dynamic variable '%s' in frozen environment %s", varname, this),
           e);
     }
     return this;
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FormatParser.java b/src/main/java/com/google/devtools/build/lib/syntax/FormatParser.java
index 64ad890..d4a2323 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FormatParser.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FormatParser.java
@@ -15,6 +15,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.Printer.BasePrinter;
 import java.util.List;
 import java.util.Map;
 
@@ -90,9 +91,10 @@
       History history,
       StringBuilder output)
       throws EvalException {
+    BasePrinter printer = Printer.getPrinter(output);
     if (has(chars, pos + 1, '{')) {
       // Escaped brace -> output and move to char after right brace
-      output.append("{");
+      printer.append("{");
       return 1;
     }
 
@@ -119,7 +121,7 @@
     }
 
     // Format object for output
-    output.append(Printer.str(value));
+    printer.str(value);
 
     // Advances the current position to the index of the closing brace of the
     // replacement field. Due to the definition of the enclosing for() loop,
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
index 6312093..682b15f 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FuncallExpression.java
@@ -28,6 +28,7 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkInterfaceUtils;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.syntax.EvalException.EvalExceptionWithJavaCause;
+import com.google.devtools.build.lib.syntax.Printer.BasePrinter;
 import com.google.devtools.build.lib.syntax.Runtime.NoneType;
 import com.google.devtools.build.lib.util.Pair;
 import com.google.devtools.build.lib.util.Preconditions;
@@ -269,15 +270,15 @@
 
   @Override
   public String toString() {
-    StringBuilder sb = new StringBuilder();
+    BasePrinter printer = Printer.getPrinter();
     if (obj != null) {
-      sb.append(obj).append(".");
+      printer.append(obj.toString()).append(".");
     }
-    sb.append(func);
-    Printer.printList(sb, args, "(", ", ", ")", /* singletonTerminator */ null,
+    printer.append(func.toString());
+    printer.printAbbreviatedList(args, "(", ", ", ")", null,
         Printer.SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT,
         Printer.SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH);
-    return sb.toString();
+    return printer.toString();
   }
 
   /**
@@ -326,7 +327,8 @@
               loc,
               "method invocation returned None, please file a bug report: "
                   + methodName
-                  + Printer.listString(ImmutableList.copyOf(args), "(", ", ", ")", null));
+                  + Printer.printAbbreviatedList(
+                      ImmutableList.copyOf(args), "(", ", ", ")", null));
         }
       }
       // TODO(bazel-team): get rid of this, by having everyone use the Skylark data structures
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/FunctionSignature.java b/src/main/java/com/google/devtools/build/lib/syntax/FunctionSignature.java
index 9b0fb00..8517a3e 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/FunctionSignature.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/FunctionSignature.java
@@ -19,6 +19,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Interner;
 import com.google.devtools.build.lib.concurrent.BlazeInterners;
+import com.google.devtools.build.lib.syntax.Printer.BasePrinter;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
 import com.google.devtools.build.lib.util.Preconditions;
 import com.google.devtools.build.lib.util.StringCanonicalizer;
@@ -393,6 +394,7 @@
         final boolean skipFirstMandatory) {
       FunctionSignature signature = getSignature();
       Shape shape = signature.getShape();
+      final BasePrinter printer = Printer.getPrinter(sb);
       final ImmutableList<String> names = signature.getNames();
       @Nullable final List<V> defaultValues = getDefaultValues();
       @Nullable final List<T> types = getTypes();
@@ -418,7 +420,7 @@
 
         public void comma() {
           if (isMore) {
-            sb.append(", ");
+            printer.append(", ");
           }
           isMore = true;
         }
@@ -436,26 +438,26 @@
           } else {
             // We only append colons when there is a name.
             if (showNames) {
-              sb.append(": ");
+              printer.append(": ");
             }
-            sb.append(typeString);
+            printer.append(typeString);
           }
         }
         public void mandatory(int i) {
           comma();
           if (showNames) {
-            sb.append(names.get(i));
+            printer.append(names.get(i));
           }
           type(i);
         }
         public void optional(int i) {
           mandatory(i);
           if (showDefaults) {
-            sb.append(" = ");
+            printer.append(" = ");
             if (defaultValues == null) {
-              sb.append("?");
+              printer.append("?");
             } else {
-              Printer.write(sb, defaultValues.get(j++));
+              printer.repr(defaultValues.get(j++));
             }
           }
         }
@@ -472,9 +474,9 @@
       }
       if (hasStar) {
         show.comma();
-        sb.append("*");
+        printer.append("*");
         if (starArg && showNames) {
-          sb.append(names.get(iStarArg));
+          printer.append(names.get(iStarArg));
         }
       }
       for (; i < endMandatoryNamedOnly; i++) {
@@ -485,9 +487,9 @@
       }
       if (kwArg) {
         show.comma();
-        sb.append("**");
+        printer.append("**");
         if (showNames) {
-          sb.append(names.get(iKwArg));
+          printer.append(names.get(iKwArg));
         }
       }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java b/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java
index a95c7de..260c318 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/GlobList.java
@@ -21,9 +21,9 @@
 import com.google.common.collect.ImmutableList.Builder;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.util.Preconditions;
-
 import java.util.ArrayList;
 import java.util.List;
 
@@ -132,7 +132,7 @@
   }
 
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.printList(buffer, this, false, quotationMark);
+  public void repr(SkylarkPrinter printer) {
+    printer.printList(this, false);
   }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java
index fbf145b..47a6ea6 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/ListLiteral.java
@@ -88,10 +88,11 @@
 
   @Override
   public String toString() {
-    StringBuilder sb = new StringBuilder();
-    Printer.printList(sb, exprs, isTuple(), '"', Printer.SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT,
+    return Printer.printAbbreviatedList(
+        exprs,
+        isTuple(),
+        Printer.SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT,
         Printer.SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH);
-    return sb.toString();
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
index 3b1a7c0..5c5905b 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
@@ -1613,25 +1613,35 @@
         }
       };
 
-  @SkylarkSignature(name = "str", returnType = String.class, doc =
-      "Converts any object to string. This is useful for debugging."
-      + "<pre class=\"language-python\">str(\"ab\") == \"ab\"</pre>",
-      parameters = {@Param(name = "x", doc = "The object to convert.")})
-  private static final BuiltinFunction str = new BuiltinFunction("str") {
-    public String invoke(Object x) {
-      return Printer.str(x);
-    }
-  };
+  @SkylarkSignature(
+    name = "str",
+    returnType = String.class,
+    doc =
+        "Converts any object to string. This is useful for debugging."
+            + "<pre class=\"language-python\">str(\"ab\") == \"ab\"</pre>",
+    parameters = {@Param(name = "x", doc = "The object to convert.")}
+  )
+  private static final BuiltinFunction str =
+      new BuiltinFunction("str") {
+        public String invoke(Object x) {
+          return Printer.getPrinter().str(x).toString();
+        }
+      };
 
-  @SkylarkSignature(name = "repr", returnType = String.class, doc =
-      "Converts any object to a string representation. This is useful for debugging.<br>"
-      + "<pre class=\"language-python\">str(\"ab\") == \\\"ab\\\"</pre>",
-      parameters = {@Param(name = "x", doc = "The object to convert.")})
-  private static final BuiltinFunction repr = new BuiltinFunction("repr") {
-    public String invoke(Object x) {
-      return Printer.repr(x);
-    }
-  };
+  @SkylarkSignature(
+    name = "repr",
+    returnType = String.class,
+    doc =
+        "Converts any object to a string representation. This is useful for debugging.<br>"
+            + "<pre class=\"language-python\">str(\"ab\") == \\\"ab\\\"</pre>",
+    parameters = {@Param(name = "x", doc = "The object to convert.")}
+  )
+  private static final BuiltinFunction repr =
+      new BuiltinFunction("repr") {
+        public String invoke(Object x) {
+          return Printer.getPrinter().repr(x).toString();
+        }
+      };
 
   @SkylarkSignature(name = "bool", returnType = Boolean.class,
       doc = "Constructor for the bool type. "
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Printer.java b/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
index fde22dc..0a58177 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Printer.java
@@ -16,7 +16,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrintableValue;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
 import com.google.devtools.build.lib.vfs.PathFragment;
@@ -28,394 +30,167 @@
 import java.util.MissingFormatWidthException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import javax.annotation.Nullable;
 
-/**
- * (Pretty) Printing of Skylark values
- */
-public final class Printer {
+/** (Pretty) Printing of Skylark values */
+public class Printer {
 
-  private static final char SKYLARK_QUOTATION_MARK = '"';
+  public static final char SKYLARK_QUOTATION_MARK = '"';
 
   /*
-   * Suggested maximum number of list elements that should be printed via printList().
+   * Suggested maximum number of list elements that should be printed via printAbbreviatedList().
    * By default, this setting is not considered and no limitation takes place.
    */
-  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT = 4;
+  public static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT = 4;
 
   /*
-   * Suggested limit for printList() to shorten the values of list elements when their combined
-   * string length reaches this value.
+   * Suggested limit for printAbbreviatedList() to shorten the values of list elements when
+   * their combined string length reaches this value.
    * By default, this setting is not considered and no limitation takes place.
    */
-  static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH = 32;
+  public static final int SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH = 32;
 
-  private Printer() {
+  /**
+   * Creates an instance of BasePrinter that wraps an existing buffer.
+   * @param buffer an Appendable
+   * @return new BasePrinter
+   */
+  public static BasePrinter getPrinter(Appendable buffer) {
+    return new BasePrinter(buffer);
   }
 
   /**
-   * Get an informal representation of object x.
-   * Currently only differs from repr in the behavior for strings and labels at top-level,
-   * that are returned as is rather than quoted.
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return the representation.
+   * Creates an instance of BasePrinter with an empty buffer.
+   * @return new BasePrinter
    */
-  public static String str(Object x, char quotationMark) {
-    return print(new StringBuilder(), x, quotationMark).toString();
+  public static BasePrinter getPrinter() {
+    return getPrinter(new StringBuilder());
   }
 
+  private Printer() {}
+
+  // These static methods proxy to the similar methods of BasePrinter
+
+  /**
+   * Format an object with Skylark's {@code str}.
+   */
   public static String str(Object x) {
-    return str(x, SKYLARK_QUOTATION_MARK);
+    return getPrinter().str(x).toString();
   }
 
   /**
-   * Get an official representation of object x.
-   * For regular data structures, the value should be parsable back into an equal data structure.
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return the representation.
+   * Format an object with Skylark's {@code repr}.
    */
-  public static String repr(Object x, char quotationMark) {
-    return write(new StringBuilder(), x, quotationMark).toString();
-  }
-
   public static String repr(Object x) {
-    return repr(x, SKYLARK_QUOTATION_MARK);
-  }
-
-  // In absence of a Python naming tradition, the write() vs print() function names
-  // follow the Lisp tradition: print() displays the informal representation (as in Python str)
-  // whereas write() displays a readable representation (as in Python repr).
-  /**
-   * Print an informal representation of object x.
-   * Currently only differs from repr in the behavior for strings and labels at top-level,
-   * that are returned as is rather than quoted.
-   * @param buffer the Appendable to which to print the representation
-   * @param o the object
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return the buffer, in fluent style
-   */
-  private static Appendable print(Appendable buffer, Object o, char quotationMark) {
-    if (o instanceof SkylarkPrintableValue) {
-      ((SkylarkPrintableValue) o).print(buffer, quotationMark);
-      return buffer;
-    }
-
-    if (o instanceof String) {
-      return append(buffer, (String) o);
-    }
-    return write(buffer, o, quotationMark);
-  }
-
-  private static Appendable print(Appendable buffer, Object o) {
-    return print(buffer, o, SKYLARK_QUOTATION_MARK);
-  }
-
-  /**
-   * Print an official representation of object x.
-   * For regular data structures, the value should be parsable back into an equal data structure.
-   * @param buffer the Appendable to write to.
-   * @param o the string a representation of which to write.
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return the Appendable, in fluent style.
-   */
-  public static Appendable write(Appendable buffer, Object o, char quotationMark) {
-    if (o == null) {
-      throw new NullPointerException(); // Java null is not a build language value.
-
-    } else if (o instanceof SkylarkValue) {
-      ((SkylarkValue) o).write(buffer, quotationMark);
-
-    } else if (o instanceof String) {
-      writeString(buffer, (String) o, quotationMark);
-
-    } else if (o instanceof Integer || o instanceof Double) {
-      append(buffer, o.toString());
-
-    } else if (o == Boolean.TRUE) {
-      append(buffer, "True");
-
-    } else if (o == Boolean.FALSE) {
-      append(buffer, "False");
-
-    } else if (o instanceof List<?>) {
-      List<?> seq = (List<?>) o;
-      printList(buffer, seq, false, quotationMark);
-
-    } else if (o instanceof Map<?, ?>) {
-      Map<?, ?> dict = (Map<?, ?>) o;
-      printList(buffer, dict.entrySet(), "{", ", ", "}", null, quotationMark);
-
-    } else if (o instanceof Map.Entry<?, ?>) {
-      Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
-      write(buffer, entry.getKey(), quotationMark);
-      append(buffer, ": ");
-      write(buffer, entry.getValue(), quotationMark);
-
-    } else if (o instanceof PathFragment) {
-      append(buffer, ((PathFragment) o).getPathString());
-
-    } else if (o instanceof Class<?>) {
-      append(buffer, EvalUtils.getDataTypeNameFromClass((Class<?>) o));
-
-    } else {
-      append(buffer, o.toString());
-    }
-
-    return buffer;
-  }
-
-  public static Appendable write(Appendable buffer, Object o) {
-    return write(buffer, o, SKYLARK_QUOTATION_MARK);
-  }
-
-  // Throughout this file, we transform IOException into AssertionError.
-  // During normal operations, we only use in-memory Appendable-s that
-  // cannot cause an IOException.
-  public static Appendable append(Appendable buffer, char c) {
-    try {
-      return buffer.append(c);
-    } catch (IOException e) {
-      throw new AssertionError(e);
-    }
-  }
-
-  public static Appendable append(Appendable buffer, CharSequence s) {
-    try {
-      return buffer.append(s);
-    } catch (IOException e) {
-      throw new AssertionError(e);
-    }
-  }
-
-  private static Appendable append(Appendable buffer, CharSequence s, int start, int end) {
-    try {
-      return buffer.append(s, start, end);
-    } catch (IOException e) {
-      throw new AssertionError(e);
-    }
-  }
-
-  private static Appendable backslashChar(Appendable buffer, char c) {
-    return append(append(buffer, '\\'), c);
-  }
-
-  private static Appendable escapeCharacter(Appendable buffer, char c, char quote) {
-    if (c == quote) {
-      return backslashChar(buffer, c);
-    }
-    switch (c) {
-      case '\\':
-        return backslashChar(buffer, '\\');
-      case '\r':
-        return backslashChar(buffer, 'r');
-      case '\n':
-        return backslashChar(buffer, 'n');
-      case '\t':
-        return backslashChar(buffer, 't');
-      default:
-        if (c < 32) {
-          return append(buffer, String.format("\\x%02x", (int) c));
-        }
-        return append(buffer, c); // no need to support UTF-8
-    } // endswitch
-  }
-
-  /**
-   * Write a properly escaped Skylark representation of a string to a buffer.
-   *
-   * @param buffer the Appendable to write to.
-   * @param s the string a representation of which to write.
-   * @param quote the quote character to use, '"' or '\''.
-   * @return the Appendable, in fluent style.
-   */
-  private static Appendable writeString(Appendable buffer, String s, char quote) {
-    append(buffer, quote);
-    int len = s.length();
-    for (int i = 0; i < len; i++) {
-      char c = s.charAt(i);
-      escapeCharacter(buffer, c, quote);
-    }
-    return append(buffer, quote);
-  }
-
-  /**
-   * Print a list of object representations
-   * @param buffer an appendable buffer onto which to write the list.
-   * @param list the list of objects to write (each as with repr)
-   * @param before a string to print before the list
-   * @param separator a separator to print between each object
-   * @param after a string to print after the list
-   * @param singletonTerminator null or a string to print after the list if it is a singleton
-   * The singleton case is notably relied upon in python syntax to distinguish
-   * a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return the Appendable, in fluent style.
-   */
-  public static Appendable printList(
-      Appendable buffer,
-      Iterable<?> list,
-      String before,
-      String separator,
-      String after,
-      String singletonTerminator,
-      char quotationMark) {
-    return printList(
-        buffer, list, before, separator, after, singletonTerminator, quotationMark, -1, -1);
+    return getPrinter().repr(x).toString();
   }
 
   /**
    * Print a list of object representations.
    *
-   * <p>The length of the output will be limited when both {@code maxItemsToPrint} and {@code
-   * criticalItemsStringLength} have values greater than zero.
+   * <p>The length of the output will be limited when both {@code maxItemsToPrint} and
+   * {@code criticalItemsStringLength} have values greater than zero.
    *
-   * @param buffer an appendable buffer onto which to write the list.
-   * @param list the list of objects to write (each as with repr)
+   * @param list the list of objects to repr (each as with repr)
    * @param before a string to print before the list
    * @param separator a separator to print between each object
    * @param after a string to print after the list
-   * @param singletonTerminator null or a string to print after the list if it is a singleton
-   * The singleton case is notably relied upon in python syntax to distinguish
-   *    a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
-   * @param quotationMark The quotation mark to be used (' or ")
+   * @param singletonTerminator null or a string to print after the list if it is a singleton The
+   *     singleton case is notably relied upon in python syntax to distinguish a tuple of size one
+   *     such as ("foo",) from a merely parenthesized object such as ("foo").
    * @param maxItemsToPrint the maximum number of elements to be printed.
    * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
-   *    'Soft' means that this limit may be exceeded because of formatting.
-   * @return the Appendable, in fluent style.
+   *     'Soft' means that this limit may be exceeded because of formatting.
+   * @return string representation.
    */
-  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
-      String separator, String after, String singletonTerminator, char quotationMark,
-      int maxItemsToPrint, int criticalItemsStringLength) {
-    append(buffer, before);
-    int len = 0;
-    // Limits the total length of the string representation of the elements, if specified.
-    if (maxItemsToPrint > 0 && criticalItemsStringLength > 0) {
-      len = appendListElements(LengthLimitedAppendable.create(buffer, criticalItemsStringLength),
-          list, separator, quotationMark, maxItemsToPrint);
-    } else {
-      len = appendListElements(buffer, list, separator, quotationMark);
-    }
-    if (singletonTerminator != null && len == 1) {
-      append(buffer, singletonTerminator);
-    }
-    return append(buffer, after);
-  }
-
-  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
-      String separator, String after, String singletonTerminator, int maxItemsToPrint,
+  public static String printAbbreviatedList(
+      Iterable<?> list,
+      String before,
+      String separator,
+      String after,
+      @Nullable String singletonTerminator,
+      int maxItemsToPrint,
       int criticalItemsStringLength) {
-    return printList(buffer, list, before, separator, after, singletonTerminator,
-        SKYLARK_QUOTATION_MARK, maxItemsToPrint, criticalItemsStringLength);
+    return getPrinter()
+        .printAbbreviatedList(
+            list,
+            before,
+            separator,
+            after,
+            singletonTerminator,
+            maxItemsToPrint,
+            criticalItemsStringLength)
+        .toString();
   }
 
   /**
-   * Appends the given elements to the specified {@link Appendable} and returns the number of
-   * elements.
-   */
-  private static int appendListElements(
-      Appendable appendable, Iterable<?> list, String separator, char quotationMark) {
-    boolean printSeparator = false; // don't print the separator before the first element
-    int len = 0;
-    for (Object o : list) {
-      if (printSeparator) {
-        append(appendable, separator);
-      }
-      write(appendable, o, quotationMark);
-      printSeparator = true;
-      len++;
-    }
-    return len;
-  }
-
-  /**
-   * Tries to append the given elements to the specified {@link Appendable} until specific limits
-   * are reached.
-   * @return the number of appended elements.
-   */
-  private static int appendListElements(LengthLimitedAppendable appendable, Iterable<?> list,
-      String separator, char quotationMark, int maxItemsToPrint) {
-    boolean printSeparator = false; // don't print the separator before the first element
-    boolean skipArgs = false;
-    int items = Iterables.size(list);
-    int len = 0;
-    // We don't want to print "1 more arguments", hence we don't skip arguments if there is only one
-    // above the limit.
-    int itemsToPrint = (items - maxItemsToPrint == 1) ? items : maxItemsToPrint;
-    appendable.enforceLimit();
-    for (Object o : list) {
-      // We don't want to print "1 more arguments", even if we hit the string limit.
-      if (len == itemsToPrint || (appendable.hasHitLimit() && len < items - 1)) {
-        skipArgs = true;
-        break;
-      }
-      if (printSeparator) {
-        append(appendable, separator);
-      }
-      write(appendable, o, quotationMark);
-      printSeparator = true;
-      len++;
-    }
-    appendable.ignoreLimit();
-    if (skipArgs) {
-      append(appendable, separator);
-      append(appendable, String.format("<%d more arguments>", items - len));
-    }
-    return len;
-  }
-
-  public static Appendable printList(Appendable buffer, Iterable<?> list, String before,
-      String separator, String after, String singletonTerminator) {
-    return printList(
-        buffer, list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
-  }
-
-  /**
-   * Print a Skylark list or tuple of object representations
-   * @param buffer an appendable buffer onto which to write the list.
-   * @param list the contents of the list or tuple
-   * @param isTuple is it a tuple or a list?
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @param maxItemsToPrint the maximum number of elements to be printed.
-   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
-   * 'Soft' means that this limit may be exceeded because of formatting.
-   * @return the Appendable, in fluent style.
-   */
-  public static Appendable printList(Appendable buffer, Iterable<?> list, boolean isTuple,
-      char quotationMark, int maxItemsToPrint, int criticalItemsStringLength) {
-    if (isTuple) {
-      return printList(buffer, list, "(", ", ", ")", ",", quotationMark, maxItemsToPrint,
-          criticalItemsStringLength);
-    } else {
-      return printList(buffer, list, "[", ", ", "]", null, quotationMark, maxItemsToPrint,
-          criticalItemsStringLength);
-    }
-  }
-
-  public static Appendable printList(
-      Appendable buffer, Iterable<?> list, boolean isTuple, char quotationMark) {
-    return printList(buffer, list, isTuple, quotationMark, -1, -1);
-  }
-
-  /**
-   * Print a list of object representations
-   * @param list the list of objects to write (each as with repr)
+   * Print a list of object representations.
+   *
+   * @param list the list of objects to repr (each as with repr)
    * @param before a string to print before the list
    * @param separator a separator to print between each object
    * @param after a string to print after the list
-   * @param singletonTerminator null or a string to print after the list if it is a singleton
-   * The singleton case is notably relied upon in python syntax to distinguish
-   * a tuple of size one such as ("foo",) from a merely parenthesized object such as ("foo").
-   * @param quotationMark The quotation mark to be used (' or ")
-   * @return a String, the representation.
+   * @param singletonTerminator null or a string to print after the list if it is a singleton The
+   *     singleton case is notably relied upon in python syntax to distinguish a tuple of size one
+   *     such as ("foo",) from a merely parenthesized object such as ("foo").
+   * @return string representation.
    */
-  public static String listString(Iterable<?> list, String before, String separator, String after,
-      String singletonTerminator, char quotationMark) {
-    return printList(new StringBuilder(), list, before, separator, after, singletonTerminator,
-               quotationMark).toString();
+    public static String printAbbreviatedList(
+      Iterable<?> list,
+      String before,
+      String separator,
+      String after,
+      @Nullable String singletonTerminator) {
+    return printAbbreviatedList(list, before, separator, after, singletonTerminator,
+        SUGGESTED_CRITICAL_LIST_ELEMENTS_COUNT, SUGGESTED_CRITICAL_LIST_ELEMENTS_STRING_LENGTH);
   }
 
-  public static String listString(
-      Iterable<?> list, String before, String separator, String after, String singletonTerminator) {
-    return listString(list, before, separator, after, singletonTerminator, SKYLARK_QUOTATION_MARK);
+  /**
+   * Print a list of object representations.
+   *
+   * <p>The length of the output will be limited when both {@code maxItemsToPrint} and
+   * {@code criticalItemsStringLength} have values greater than zero.
+   *
+   * @param list the list of objects to repr (each as with repr)
+   * @param isTuple if true the list will be formatted with parentheses and with a trailing comma
+   *     in case of one-element tuples.
+   * @param maxItemsToPrint the maximum number of elements to be printed.
+   * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
+   *     'Soft' means that this limit may be exceeded because of formatting.
+   * @return string representation.
+   */
+  public static String printAbbreviatedList(
+      Iterable<?> list,
+      boolean isTuple,
+      int maxItemsToPrint,
+      int criticalItemsStringLength) {
+    return getPrinter()
+        .printAbbreviatedList(list, isTuple, maxItemsToPrint, criticalItemsStringLength)
+        .toString();
+  }
+
+  /**
+   * Perform Python-style string formatting, as per pattern % tuple Limitations: only %d %s %r %%
+   * are supported.
+   *
+   * @param pattern a format string.
+   * @param arguments an array containing positional arguments.
+   * @return the formatted string.
+   */
+  public static String format(String pattern, Object... arguments) {
+    return getPrinter().format(pattern, arguments).toString();
+  }
+
+  /**
+   * Perform Python-style string formatting, as per pattern % tuple Limitations: only %d %s %r %%
+   * are supported.
+   *
+   * @param pattern a format string.
+   * @param arguments a tuple containing positional arguments.
+   * @return the formatted string.
+   */
+  public static String formatWithList(String pattern, List<?> arguments) {
+    return getPrinter().formatWithList(pattern, arguments).toString();
   }
 
   /**
@@ -428,115 +203,57 @@
   public static Formattable formattable(final String pattern, Object... arguments) {
     final ImmutableList<Object> args = ImmutableList.copyOf(arguments);
     return new Formattable() {
-        @Override
-        public String toString() {
-          return formatToString(pattern, args);
-        }
+      @Override
+      public String toString() {
+        return formatWithList(pattern, args);
+      }
 
-        @Override
-        public void formatTo(Formatter formatter, int flags, int width, int precision) {
-          Printer.formatTo(formatter.out(), pattern, args);
-        }
-      };
+      @Override
+      public void formatTo(Formatter formatter, int flags, int width, int precision) {
+        Printer.getPrinter(formatter.out()).formatWithList(pattern, args);
+      }
+    };
   }
 
   /**
-   * Perform Python-style string formatting.
+   * Append a char to a buffer. In case of {@link IOException} throw an {@link AssertionError}
+   * instead
    *
-   * @param pattern a format string.
-   * @param arguments a tuple containing positional arguments.
-   * @return the formatted string.
+   * @return buffer
    */
-  public static String format(String pattern, Object... arguments) {
-    return formatToString(pattern, ImmutableList.copyOf(arguments));
-  }
-
-  /**
-   * Perform Python-style string formatting.
-   *
-   * @param pattern a format string.
-   * @param arguments a tuple containing positional arguments.
-   * @return the formatted string.
-   */
-  public static String formatToString(String pattern, List<?> arguments) {
-    return formatTo(new StringBuilder(), pattern, arguments).toString();
-  }
-
-  /**
-   * Perform Python-style string formatting, as per pattern % tuple
-   * Limitations: only %d %s %r %% are supported.
-   *
-   * @param buffer an Appendable to output to.
-   * @param pattern a format string.
-   * @param arguments a list containing positional arguments.
-   * @return the buffer, in fluent style.
-   */
-  // TODO(bazel-team): support formatting arguments, and more complex Python patterns.
-  public static Appendable formatTo(Appendable buffer, String pattern, List<?> arguments) {
-    // N.B. MissingFormatWidthException is the only kind of IllegalFormatException
-    // whose constructor can take and display arbitrary error message, hence its use below.
-
-    int length = pattern.length();
-    int argLength = arguments.size();
-    int i = 0; // index of next character in pattern
-    int a = 0; // index of next argument in arguments
-
-    while (i < length) {
-      int p = pattern.indexOf('%', i);
-      if (p == -1) {
-        append(buffer, pattern, i, length);
-        break;
-      }
-      if (p > i) {
-        append(buffer, pattern, i, p);
-      }
-      if (p == length - 1) {
-        throw new MissingFormatWidthException(
-            "incomplete format pattern ends with %: " + repr(pattern));
-      }
-      char directive = pattern.charAt(p + 1);
-      i = p + 2;
-      switch (directive) {
-        case '%':
-          append(buffer, '%');
-          continue;
-        case 'd':
-        case 'r':
-        case 's':
-          if (a >= argLength) {
-            throw new MissingFormatWidthException("not enough arguments for format pattern "
-                + repr(pattern) + ": "
-                + repr(Tuple.copyOf(arguments)));
-          }
-          Object argument = arguments.get(a++);
-          switch (directive) {
-            case 'd':
-              if (argument instanceof Integer) {
-                append(buffer, argument.toString());
-                continue;
-              } else {
-                throw new MissingFormatWidthException(
-                    "invalid argument " + repr(argument) + " for format pattern %d");
-              }
-            case 'r':
-              write(buffer, argument);
-              continue;
-            case 's':
-              print(buffer, argument);
-              continue;
-          }
-          // fall through
-        default:
-          throw new MissingFormatWidthException(
-              "unsupported format character " + repr(String.valueOf(directive))
-              + " at index " + (p + 1) + " in " + repr(pattern));
-      }
+  public static Appendable append(Appendable buffer, char c) {
+    try {
+      return buffer.append(c);
+    } catch (IOException e) {
+      throw new AssertionError(e);
     }
-    if (a < argLength) {
-      throw new MissingFormatWidthException(
-          "not all arguments converted during string formatting");
+  }
+
+  /**
+   * Append a char sequence to a buffer. In case of {@link IOException} throw an
+   * {@link AssertionError} instead
+   *
+   * @return buffer
+   */
+  public static Appendable append(Appendable buffer, CharSequence s) {
+    try {
+      return buffer.append(s);
+    } catch (IOException e) {
+      throw new AssertionError(e);
     }
-    return buffer;
+  }
+
+  /**
+   * Append a char sequence range to a buffer. In case of {@link IOException} throw an
+   * {@link AssertionError} instead
+   * @return buffer
+   */
+  private static Appendable append(Appendable buffer, CharSequence s, int start, int end) {
+    try {
+      return buffer.append(s, start, end);
+    } catch (IOException e) {
+      throw new AssertionError(e);
+    }
   }
 
   /**
@@ -563,7 +280,7 @@
       this.limit = limit;
     }
 
-    public static LengthLimitedAppendable create(Appendable original, int limit) {
+    private static LengthLimitedAppendable create(Appendable original, int limit) {
       // We don't want to overwrite the limit if original is already an instance of this class.
       return (original instanceof LengthLimitedAppendable)
           ? (LengthLimitedAppendable) original : new LengthLimitedAppendable(original, limit);
@@ -650,12 +367,12 @@
 
     @Override
     public Appendable append(CharSequence csq, int start, int end) throws IOException {
-      return append(csq.subSequence(start, end));
+      return this.append(csq.subSequence(start, end));
     }
 
     @Override
     public Appendable append(char c) throws IOException {
-      return append(String.valueOf(c));
+      return this.append(String.valueOf(c));
     }
     
     public boolean hasHitLimit()  {
@@ -675,4 +392,404 @@
       return original.toString();
     }
   }
+
+  /** Actual class that implements Printer API */
+  public static final class BasePrinter implements SkylarkPrinter {
+    // Methods of this class should not recurse through static methods of Printer
+
+    private final Appendable buffer;
+
+    /**
+     * Creates a printer instance.
+     *
+     * @param buffer the Appendable to which to print the representation
+     */
+    private BasePrinter(Appendable buffer) {
+      this.buffer = buffer;
+    }
+
+    @Override
+    public String toString() {
+      return buffer.toString();
+    }
+
+    /**
+     * Print an informal representation of object x. Currently only differs from repr in the
+     * behavior for strings and labels at top-level, that are returned as is rather than quoted.
+     *
+     * @param o the object
+     * @return the buffer, in fluent style
+     */
+    public BasePrinter str(Object o) {
+      if (o instanceof SkylarkPrintableValue) {
+        ((SkylarkPrintableValue) o).str(this);
+        return this;
+      }
+
+      if (o instanceof String) {
+        return this.append((String) o);
+      }
+      return this.repr(o);
+    }
+
+    /**
+     * Print an official representation of object x. For regular data structures, the value should
+     * be parsable back into an equal data structure.
+     *
+     * @param o the string a representation of which to repr.
+     * @return BasePrinter.
+     */
+    @Override
+    public BasePrinter repr(Object o) {
+      if (o == null) {
+        throw new NullPointerException(); // Java null is not a valid Skylark value.
+
+      } else if (o instanceof SkylarkValue) {
+        ((SkylarkValue) o).repr(this);
+
+      } else if (o instanceof String) {
+        writeString((String) o);
+
+      } else if (o instanceof Integer || o instanceof Double) {
+        this.append(o.toString());
+
+      } else if (o == Boolean.TRUE) {
+        this.append("True");
+
+      } else if (o == Boolean.FALSE) {
+        this.append("False");
+
+      } else if (o instanceof Map<?, ?>) {
+        Map<?, ?> dict = (Map<?, ?>) o;
+        this.printList(dict.entrySet(), "{", ", ", "}", null);
+
+      } else if (o instanceof List<?>) {
+        List<?> seq = (List<?>) o;
+        this.printList(seq, false);
+
+      } else if (o instanceof Map.Entry<?, ?>) {
+        Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
+        this.repr(entry.getKey());
+        this.append(": ");
+        this.repr(entry.getValue());
+
+      } else if (o instanceof PathFragment) {
+        this.append(((PathFragment) o).getPathString());
+
+      } else if (o instanceof Class<?>) {
+        this.append(EvalUtils.getDataTypeNameFromClass((Class<?>) o));
+
+      } else if (o instanceof ASTNode || o instanceof Location) {
+        // AST node objects and locations are printed in tracebacks and error messages,
+        // it's safe to print their toString representations
+        this.append(o.toString());
+
+      } else {
+        // TODO(bazel-team): change to a special representation for unknown objects
+        this.append(o.toString());
+      }
+
+      return this;
+    }
+
+    /**
+     * Write a properly escaped Skylark representation of a string to a buffer.
+     *
+     * @param s the string a representation of which to repr.
+     * @return the Appendable, in fluent style.
+     */
+    private BasePrinter writeString(String s) {
+      this.append(SKYLARK_QUOTATION_MARK);
+      int len = s.length();
+      for (int i = 0; i < len; i++) {
+        char c = s.charAt(i);
+        escapeCharacter(c);
+      }
+      return this.append(SKYLARK_QUOTATION_MARK);
+    }
+
+    private BasePrinter backslashChar(char c) {
+      return this.append('\\').append(c);
+    }
+
+    private BasePrinter escapeCharacter(char c) {
+      if (c == SKYLARK_QUOTATION_MARK) {
+        return backslashChar(c);
+      }
+      switch (c) {
+        case '\\':
+          return backslashChar('\\');
+        case '\r':
+          return backslashChar('r');
+        case '\n':
+          return backslashChar('n');
+        case '\t':
+          return backslashChar('t');
+        default:
+          if (c < 32) {
+            //TODO(bazel-team): support \x escapes
+            return this.append(String.format("\\x%02x", (int) c));
+          }
+          return this.append(c); // no need to support UTF-8
+      } // endswitch
+    }
+
+    /**
+     * Print a list of object representations
+     *
+     * @param list the list of objects to repr (each as with repr)
+     * @param before a string to print before the list items, e.g. an opening bracket
+     * @param separator a separator to print between items
+     * @param after a string to print after the list items, e.g. a closing bracket
+     * @param singletonTerminator null or a string to print after the list if it is a singleton The
+     *     singleton case is notably relied upon in python syntax to distinguish a tuple of size one
+     *     such as ("foo",) from a merely parenthesized object such as ("foo").
+     * @return the BasePrinter.
+     */
+    @Override
+    public BasePrinter printList(
+        Iterable<?> list,
+        String before,
+        String separator,
+        String after,
+        @Nullable String singletonTerminator) {
+      return printAbbreviatedList(list, before, separator, after, singletonTerminator, -1, -1);
+    }
+
+    /**
+     * Print a list of object representations.
+     *
+     * <p>The length of the output will be limited when both {@code maxItemsToPrint} and {@code
+     * criticalItemsStringLength} have values greater than zero.
+     *
+     * @param list the list of objects to repr (each as with repr)
+     * @param before a string to print before the list
+     * @param separator a separator to print between each object
+     * @param after a string to print after the list
+     * @param singletonTerminator null or a string to print after the list if it is a singleton The
+     *     singleton case is notably relied upon in python syntax to distinguish a tuple of size one
+     *     such as ("foo",) from a merely parenthesized object such as ("foo").
+     * @param maxItemsToPrint the maximum number of elements to be printed.
+     * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
+     *     'Soft' means that this limit may be exceeded because of formatting.
+     * @return the BasePrinter.
+     */
+    public BasePrinter printAbbreviatedList(
+        Iterable<?> list,
+        String before,
+        String separator,
+        String after,
+        @Nullable String singletonTerminator,
+        int maxItemsToPrint,
+        int criticalItemsStringLength) {
+      this.append(before);
+      int len = 0;
+      // Limits the total length of the string representation of the elements, if specified.
+      if (maxItemsToPrint > 0 && criticalItemsStringLength > 0) {
+        len =
+            appendListElements(
+                LengthLimitedAppendable.create(buffer, criticalItemsStringLength),
+                list,
+                separator,
+                maxItemsToPrint);
+      } else {
+        len = appendListElements(list, separator);
+      }
+      if (singletonTerminator != null && len == 1) {
+        this.append(singletonTerminator);
+      }
+      return this.append(after);
+    }
+
+    /**
+     * Appends the given elements to the specified {@link Appendable} and returns the number of
+     * elements.
+     */
+    private int appendListElements(Iterable<?> list, String separator) {
+      boolean printSeparator = false; // don't print the separator before the first element
+      int len = 0;
+      for (Object o : list) {
+        if (printSeparator) {
+          this.append(separator);
+        }
+        this.repr(o);
+        printSeparator = true;
+        len++;
+      }
+      return len;
+    }
+
+    /**
+     * Tries to append the given elements to the specified {@link Appendable} until specific limits
+     * are reached.
+     *
+     * @return the number of appended elements.
+     */
+    private int appendListElements(
+        LengthLimitedAppendable appendable,
+        Iterable<?> list,
+        String separator,
+        int maxItemsToPrint) {
+      boolean printSeparator = false; // don't print the separator before the first element
+      boolean skipArgs = false;
+      int items = Iterables.size(list);
+      int len = 0;
+      // We don't want to print "1 more arguments", hence we don't skip arguments if there is only
+      // one above the limit.
+      int itemsToPrint = (items - maxItemsToPrint == 1) ? items : maxItemsToPrint;
+      appendable.enforceLimit();
+      for (Object o : list) {
+        // We don't want to print "1 more arguments", even if we hit the string limit.
+        if (len == itemsToPrint || (appendable.hasHitLimit() && len < items - 1)) {
+          skipArgs = true;
+          break;
+        }
+        if (printSeparator) {
+          this.append(separator);
+        }
+        Printer.getPrinter(appendable).repr(o);
+        printSeparator = true;
+        len++;
+      }
+      appendable.ignoreLimit();
+      if (skipArgs) {
+        this.append(separator);
+        this.append(String.format("<%d more arguments>", items - len));
+      }
+      return len;
+    }
+
+    /**
+     * Print a Skylark list or tuple of object representations
+     *
+     * @param list the contents of the list or tuple
+     * @param isTuple if true the list will be formatted with parentheses and with a trailing comma
+     *     in case of one-element tuples.
+     * @param maxItemsToPrint the maximum number of elements to be printed.
+     * @param criticalItemsStringLength a soft limit for the total string length of all arguments.
+     *     'Soft' means that this limit may be exceeded because of formatting.
+     * @return the Appendable, in fluent style.
+     */
+    public BasePrinter printAbbreviatedList(
+        Iterable<?> list, boolean isTuple, int maxItemsToPrint, int criticalItemsStringLength) {
+      if (isTuple) {
+        return this.printAbbreviatedList(list, "(", ", ", ")", ",", 
+            maxItemsToPrint, criticalItemsStringLength);
+      } else {
+        return this.printAbbreviatedList(list, "[", ", ", "]", null, 
+            maxItemsToPrint, criticalItemsStringLength);
+      }
+    }
+
+    @Override
+    public BasePrinter printList(Iterable<?> list, boolean isTuple) {
+      return this.printAbbreviatedList(list, isTuple, -1, -1);
+    }
+
+    /**
+     * Perform Python-style string formatting, as per pattern % tuple Limitations: only %d %s %r %%
+     * are supported.
+     *
+     * @param pattern a format string.
+     * @param arguments an array containing positional arguments.
+     * @return the formatted string.
+     */
+    @Override
+    public BasePrinter format(String pattern, Object... arguments) {
+      return this.formatWithList(pattern, ImmutableList.copyOf(arguments));
+    }
+
+    /**
+     * Perform Python-style string formatting, as per pattern % tuple Limitations: only %d %s %r %%
+     * are supported.
+     *
+     * @param pattern a format string.
+     * @param arguments a tuple containing positional arguments.
+     * @return the formatted string.
+     */
+    @Override
+    public BasePrinter formatWithList(String pattern, List<?> arguments) {
+      // TODO(bazel-team): support formatting arguments, and more complex Python patterns.
+      // N.B. MissingFormatWidthException is the only kind of IllegalFormatException
+      // whose constructor can take and display arbitrary error message, hence its use below.
+
+      int length = pattern.length();
+      int argLength = arguments.size();
+      int i = 0; // index of next character in pattern
+      int a = 0; // index of next argument in arguments
+
+      while (i < length) {
+        int p = pattern.indexOf('%', i);
+        if (p == -1) {
+          Printer.append(buffer, pattern, i, length);
+          break;
+        }
+        if (p > i) {
+          Printer.append(buffer, pattern, i, p);
+        }
+        if (p == length - 1) {
+          throw new MissingFormatWidthException(
+              "incomplete format pattern ends with %: " + this.repr(pattern));
+        }
+        char directive = pattern.charAt(p + 1);
+        i = p + 2;
+        switch (directive) {
+          case '%':
+            this.append('%');
+            continue;
+          case 'd':
+          case 'r':
+          case 's':
+            if (a >= argLength) {
+              throw new MissingFormatWidthException(
+                  "not enough arguments for format pattern "
+                      + this.repr(pattern)
+                      + ": "
+                      + this.repr(Tuple.copyOf(arguments)));
+            }
+            Object argument = arguments.get(a++);
+            switch (directive) {
+              case 'd':
+                if (argument instanceof Integer) {
+                  this.append(argument.toString());
+                  continue;
+                } else {
+                  throw new MissingFormatWidthException(
+                      "invalid argument " + this.repr(argument) + " for format pattern %d");
+                }
+              case 'r':
+                this.repr(argument);
+                continue;
+              case 's':
+                this.str(argument);
+                continue;
+            }
+            // fall through
+          default:
+            throw new MissingFormatWidthException(
+                // The call to Printer.repr doesn't cause an infinite recursion because it's
+                // only used to format a string properly
+                String.format("unsupported format character \"%s\" at index %s in %s",
+                    String.valueOf(directive), p + 1, Printer.repr(pattern)));
+        }
+      }
+      if (a < argLength) {
+        throw new MissingFormatWidthException(
+            "not all arguments converted during string formatting");
+      }
+      return this;
+    }
+
+    @Override
+    public BasePrinter append(char c) {
+      Printer.append(buffer, c);
+      return this;
+    }
+
+    @Override
+    public BasePrinter append(CharSequence s) {
+      Printer.append(buffer, s);
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Runtime.java b/src/main/java/com/google/devtools/build/lib/syntax/Runtime.java
index 8f67212..e3e2e87 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Runtime.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Runtime.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.Immutable;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.util.Preconditions;
@@ -61,8 +62,8 @@
     }
 
     @Override
-    public void write(Appendable buffer, char quotationMark) {
-      Printer.append(buffer, "None");
+    public void repr(SkylarkPrinter printer) {
+      printer.append("None");
     }
   }
 
@@ -82,8 +83,8 @@
     }
 
     @Override
-    public void write(Appendable buffer, char quotationMark) {
-      Printer.append(buffer, "<unbound>");
+    public void repr(SkylarkPrinter printer) {
+      printer.append("<unbound>");
     }
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java b/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
index 5e72aed..c06089b 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SelectorList.java
@@ -16,9 +16,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.util.Preconditions;
-
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
@@ -152,8 +152,8 @@
   }
 
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.printList(buffer, elements, "", " + ", "", null, quotationMark);
+  public void repr(SkylarkPrinter printer) {
+    printer.printList(elements, "", " + ", "", null);
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
index 7ff2e8b..20f409f 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SelectorValue.java
@@ -16,9 +16,9 @@
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
-
 import java.util.Map;
 import java.util.TreeMap;
 
@@ -79,8 +79,8 @@
   }
 
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.formatTo(buffer, "selector(%r)", Tuple.of(dictionary));
+  public void repr(SkylarkPrinter printer) {
+    printer.formatWithList("selector(%r)", Tuple.of(dictionary));
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
index 01f104b..bfb7dca 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkDict.java
@@ -17,6 +17,7 @@
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.SkylarkMutable.MutableMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -168,8 +169,8 @@
 
   // Other methods
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.printList(buffer, entrySet(), "{", ", ", "}", null, quotationMark);
+  public void repr(SkylarkPrinter printer) {
+    printer.printList(entrySet(), "{", ", ", "}", null);
   }
 
   /**
@@ -191,7 +192,7 @@
     }
     throw new EvalException(
         null,
-        Printer.format(
+        String.format(
             "%s is not of expected type dict or NoneType",
             description == null ? Printer.repr(obj) : String.format("'%s'", description)));
   }
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java
index 7e2ae60..c9e3eec 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkList.java
@@ -19,6 +19,7 @@
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.syntax.SkylarkMutable.MutableCollection;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -182,8 +183,8 @@
 
   // Other methods
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.printList(buffer, getContentsUnsafe(), isTuple(), quotationMark);
+  public void repr(SkylarkPrinter printer) {
+    printer.printList(getContentsUnsafe(), isTuple());
   }
 
   // Note that the following two functions slightly violate the Java List protocol,
@@ -236,7 +237,7 @@
       return ((SkylarkList<?>) obj).getContents(type, description);
     }
     throw new EvalException(null,
-        Printer.format("Illegal argument: %s is not of expected type list or NoneType",
+        String.format("Illegal argument: %s is not of expected type list or NoneType",
             description == null ? Printer.repr(obj) : String.format("'%s'", description)));
   }
 
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
index 674264c..3d8daea 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SkylarkNestedSet.java
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkValue;
 import com.google.devtools.build.lib.util.Preconditions;
 import java.util.ArrayList;
@@ -321,14 +322,15 @@
   }
 
   @Override
-  public void write(Appendable buffer, char quotationMark) {
-    Printer.append(buffer, "depset(");
-    Printer.printList(buffer, set, "[", ", ", "]", null, quotationMark);
+  public void repr(SkylarkPrinter printer) {
+    printer.append("depset(");
+    printer.printList(set, "[", ", ", "]", null);
     Order order = getOrder();
     if (order != Order.STABLE_ORDER) {
-      Printer.append(buffer, ", order = \"" + order.getSkylarkName() + "\"");
+      printer.append(", order = ");
+      printer.repr(order.getSkylarkName());
     }
-    Printer.append(buffer, ")");
+    printer.append(")");
   }
 
   @Override
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/SliceExpression.java b/src/main/java/com/google/devtools/build/lib/syntax/SliceExpression.java
index f3ef277..92d51fe 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/SliceExpression.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/SliceExpression.java
@@ -107,7 +107,7 @@
 
     throw new EvalException(
         loc,
-        Printer.format(
+        String.format(
             "type '%s' has no operator [:](%s, %s, %s)",
             EvalUtils.getDataTypeName(objValue),
             EvalUtils.getDataTypeName(startValue),
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/Type.java b/src/main/java/com/google/devtools/build/lib/syntax/Type.java
index da2750d..aac5420 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/Type.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/Type.java
@@ -21,6 +21,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.syntax.Printer.BasePrinter;
 import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
 import com.google.devtools.build.lib.util.LoggingUtil;
 import com.google.devtools.build.lib.util.StringCanonicalizer;
@@ -249,15 +250,15 @@
    */
   public static class ConversionException extends EvalException {
     private static String message(Type<?> type, Object value, @Nullable Object what) {
-      StringBuilder builder = new StringBuilder();
-      builder.append("expected value of type '").append(type).append("'");
+      BasePrinter printer = Printer.getPrinter();
+      printer.append("expected value of type '").append(type.toString()).append("'");
       if (what != null) {
-        builder.append(" for ").append(what);
+        printer.append(" for ").append(what.toString());
       }
-      builder.append(", but got ");
-      Printer.write(builder, value);
-      builder.append(" (").append(EvalUtils.getDataTypeName(value)).append(")");
-      return builder.toString();
+      printer.append(", but got ");
+      printer.repr(value);
+      printer.append(" (").append(EvalUtils.getDataTypeName(value)).append(")");
+      return printer.toString();
     }
 
     public ConversionException(Type<?> type, Object value, @Nullable Object what) {