Bug 1317375 - Implement "Template Literals Revision / Lifting Template Literal Restriction" ECMAScript proposal r=arai
authorKevin Gibbons <bakkot@gmail.com>
Thu, 19 Jan 2017 11:14:00 +0900
changeset 375344 bb868860dfc35876d2d9c421c037c75a4fb9b3d2
parent 375343 44c956b966c7e36da697fd5517450e2bc93255c3
child 375345 8ccb35efc96fd51be71e315ea0085a113235f7e6
push id6996
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 20:48:21 +0000
treeherdermozilla-beta@d89512dab048 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersarai
bugs1317375
milestone53.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1317375 - Implement "Template Literals Revision / Lifting Template Literal Restriction" ECMAScript proposal r=arai MozReview-Commit-ID: 4OBI6kCe7Lf
js/src/builtin/ReflectParse.cpp
js/src/frontend/BytecodeEmitter.cpp
js/src/frontend/FoldConstants.cpp
js/src/frontend/FullParseHandler.h
js/src/frontend/NameFunctions.cpp
js/src/frontend/ParseNode.cpp
js/src/frontend/ParseNode.h
js/src/frontend/Parser.cpp
js/src/frontend/Parser.h
js/src/frontend/SyntaxParseHandler.h
js/src/frontend/TokenStream.cpp
js/src/frontend/TokenStream.h
js/src/tests/ecma_6/TemplateStrings/tagTempl.js
js/src/tests/js1_8_5/reflect-parse/templateStrings.js
--- a/js/src/builtin/ReflectParse.cpp
+++ b/js/src/builtin/ReflectParse.cpp
@@ -3039,17 +3039,22 @@ ASTSerializer::expression(ParseNode* pn,
         NodeVector cooked(cx);
         if (!cooked.reserve(pn->pn_count - 1))
             return false;
 
         for (ParseNode* next = pn->pn_head->pn_next; next; next = next->pn_next) {
             MOZ_ASSERT(pn->pn_pos.encloses(next->pn_pos));
 
             RootedValue expr(cx);
-            expr.setString(next->pn_atom);
+            if (next->isKind(PNK_RAW_UNDEFINED)) {
+                expr.setUndefined();
+            } else {
+                MOZ_ASSERT(next->isKind(PNK_TEMPLATE_STRING));
+                expr.setString(next->pn_atom);
+            }
             cooked.infallibleAppend(expr);
         }
 
         return builder.callSiteObj(raw, cooked, &pn->pn_pos, dst);
       }
 
       case PNK_ARRAY:
       {
@@ -3131,16 +3136,17 @@ ASTSerializer::expression(ParseNode* pn,
 
       case PNK_TEMPLATE_STRING:
       case PNK_STRING:
       case PNK_REGEXP:
       case PNK_NUMBER:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
         return literal(pn, dst);
 
       case PNK_YIELD_STAR:
       {
         MOZ_ASSERT(pn->pn_pos.encloses(pn->pn_left->pn_pos));
 
         RootedValue arg(cx);
         return expression(pn->pn_left, &arg) &&
@@ -3271,16 +3277,20 @@ ASTSerializer::literal(ParseNode* pn, Mu
       case PNK_NUMBER:
         val.setNumber(pn->pn_dval);
         break;
 
       case PNK_NULL:
         val.setNull();
         break;
 
+      case PNK_RAW_UNDEFINED:
+        val.setUndefined();
+        break;
+
       case PNK_TRUE:
         val.setBoolean(true);
         break;
 
       case PNK_FALSE:
         val.setBoolean(false);
         break;
 
--- a/js/src/frontend/BytecodeEmitter.cpp
+++ b/js/src/frontend/BytecodeEmitter.cpp
@@ -2655,16 +2655,17 @@ BytecodeEmitter::checkSideEffects(ParseN
       // Trivial cases with no side effects.
       case PNK_NOP:
       case PNK_STRING:
       case PNK_TEMPLATE_STRING:
       case PNK_REGEXP:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
       case PNK_ELISION:
       case PNK_GENERATOR:
       case PNK_NUMBER:
       case PNK_OBJECT_PROPERTY_NAME:
         MOZ_ASSERT(pn->isArity(PN_NULLARY));
         *answer = false;
         return true;
 
@@ -5820,16 +5821,19 @@ ParseNode::getConstantValue(ExclusiveCon
         vp.setBoolean(true);
         return true;
       case PNK_FALSE:
         vp.setBoolean(false);
         return true;
       case PNK_NULL:
         vp.setNull();
         return true;
+      case PNK_RAW_UNDEFINED:
+        vp.setUndefined();
+        return true;
       case PNK_CALLSITEOBJ:
       case PNK_ARRAY: {
         unsigned count;
         ParseNode* pn;
 
         if (allowObjects == DontAllowObjects) {
             vp.setMagic(JS_GENERIC_MAGIC);
             return true;
@@ -10205,16 +10209,17 @@ BytecodeEmitter::emitTree(ParseNode* pn,
       case PNK_REGEXP:
         if (!emitRegExp(objectList.add(pn->as<RegExpLiteral>().objbox())))
             return false;
         break;
 
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
         if (!emit1(pn->getOp()))
             return false;
         break;
 
       case PNK_THIS:
         if (!emitThisLiteral(pn))
             return false;
         break;
--- a/js/src/frontend/FoldConstants.cpp
+++ b/js/src/frontend/FoldConstants.cpp
@@ -373,16 +373,17 @@ ContainsHoistedDeclaration(ExclusiveCont
       case PNK_TEMPLATE_STRING_LIST:
       case PNK_TAGGED_TEMPLATE:
       case PNK_CALLSITEOBJ:
       case PNK_STRING:
       case PNK_REGEXP:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
       case PNK_THIS:
       case PNK_ELISION:
       case PNK_NUMBER:
       case PNK_NEW:
       case PNK_GENERATOR:
       case PNK_GENEXP:
       case PNK_ARRAYCOMP:
       case PNK_PARAMSBODY:
@@ -463,16 +464,17 @@ static bool
 IsEffectless(ParseNode* node)
 {
     return node->isKind(PNK_TRUE) ||
            node->isKind(PNK_FALSE) ||
            node->isKind(PNK_STRING) ||
            node->isKind(PNK_TEMPLATE_STRING) ||
            node->isKind(PNK_NUMBER) ||
            node->isKind(PNK_NULL) ||
+           node->isKind(PNK_RAW_UNDEFINED) ||
            node->isKind(PNK_FUNCTION) ||
            node->isKind(PNK_GENEXP);
 }
 
 enum Truthiness { Truthy, Falsy, Unknown };
 
 static Truthiness
 Boolish(ParseNode* pn)
@@ -487,16 +489,17 @@ Boolish(ParseNode* pn)
 
       case PNK_TRUE:
       case PNK_FUNCTION:
       case PNK_GENEXP:
         return Truthy;
 
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
         return Falsy;
 
       case PNK_VOID: {
         // |void <foo>| evaluates to |undefined| which isn't truthy.  But the
         // sense of this method requires that the expression be literally
         // replaceable with true/false: not the case if the nested expression
         // is effectful, might throw, &c.  Walk past the |void| (and nested
         // |void| expressions, for good measure) and check that the nested
@@ -1638,16 +1641,17 @@ Fold(ExclusiveContext* cx, ParseNode** p
 
     switch (pn->getKind()) {
       case PNK_NOP:
       case PNK_REGEXP:
       case PNK_STRING:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
       case PNK_ELISION:
       case PNK_NUMBER:
       case PNK_DEBUGGER:
       case PNK_BREAK:
       case PNK_CONTINUE:
       case PNK_TEMPLATE_STRING:
       case PNK_GENERATOR:
       case PNK_EXPORT_BATCH_SPEC:
--- a/js/src/frontend/FullParseHandler.h
+++ b/js/src/frontend/FullParseHandler.h
@@ -178,16 +178,20 @@ class FullParseHandler
     ParseNode* newThisLiteral(const TokenPos& pos, ParseNode* thisName) {
         return new_<ThisLiteral>(pos, thisName);
     }
 
     ParseNode* newNullLiteral(const TokenPos& pos) {
         return new_<NullLiteral>(pos);
     }
 
+    ParseNode* newRawUndefinedLiteral(const TokenPos& pos) {
+        return new_<RawUndefinedLiteral>(pos);
+    }
+
     // The Boxer object here is any object that can allocate ObjectBoxes.
     // Specifically, a Boxer has a .newObjectBox(T) method that accepts a
     // Rooted<RegExpObject*> argument and returns an ObjectBox*.
     template <class Boxer>
     ParseNode* newRegExp(RegExpObject* reobj, const TokenPos& pos, Boxer& boxer) {
         ObjectBox* objbox = boxer.newObjectBox(reobj);
         if (!objbox)
             return null();
--- a/js/src/frontend/NameFunctions.cpp
+++ b/js/src/frontend/NameFunctions.cpp
@@ -311,27 +311,28 @@ class NameResolver
         ParseNode* element = node->pn_head;
 
         // The list head is a leading expression, e.g. |tag| in |tag`foo`|,
         // that might contain functions.
         if (!resolve(element, prefix))
             return false;
 
         // Next is the callsite object node.  This node only contains
-        // internal strings and an array -- no user-controlled expressions.
+        // internal strings or undefined and an array -- no user-controlled
+        // expressions.
         element = element->pn_next;
 #ifdef DEBUG
         {
             MOZ_ASSERT(element->isKind(PNK_CALLSITEOBJ));
             ParseNode* array = element->pn_head;
             MOZ_ASSERT(array->isKind(PNK_ARRAY));
             for (ParseNode* kid = array->pn_head; kid; kid = kid->pn_next)
                 MOZ_ASSERT(kid->isKind(PNK_TEMPLATE_STRING));
             for (ParseNode* next = array->pn_next; next; next = next->pn_next)
-                MOZ_ASSERT(next->isKind(PNK_TEMPLATE_STRING));
+                MOZ_ASSERT(next->isKind(PNK_TEMPLATE_STRING) || next->isKind(PNK_RAW_UNDEFINED));
         }
 #endif
 
         // Next come any interpolated expressions in the tagged template.
         ParseNode* interpolated = element->pn_next;
         for (; interpolated; interpolated = interpolated->pn_next) {
             if (!resolve(interpolated, prefix))
                 return false;
@@ -377,16 +378,17 @@ class NameResolver
           // further work.
           case PNK_NOP:
           case PNK_STRING:
           case PNK_TEMPLATE_STRING:
           case PNK_REGEXP:
           case PNK_TRUE:
           case PNK_FALSE:
           case PNK_NULL:
+          case PNK_RAW_UNDEFINED:
           case PNK_ELISION:
           case PNK_GENERATOR:
           case PNK_NUMBER:
           case PNK_BREAK:
           case PNK_CONTINUE:
           case PNK_DEBUGGER:
           case PNK_EXPORT_BATCH_SPEC:
           case PNK_OBJECT_PROPERTY_NAME:
--- a/js/src/frontend/ParseNode.cpp
+++ b/js/src/frontend/ParseNode.cpp
@@ -185,16 +185,17 @@ PushNodeChildren(ParseNode* pn, NodeStac
       // but their parents, are never used, and are never a definition.
       case PNK_NOP:
       case PNK_STRING:
       case PNK_TEMPLATE_STRING:
       case PNK_REGEXP:
       case PNK_TRUE:
       case PNK_FALSE:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
       case PNK_ELISION:
       case PNK_GENERATOR:
       case PNK_NUMBER:
       case PNK_BREAK:
       case PNK_CONTINUE:
       case PNK_DEBUGGER:
       case PNK_EXPORT_BATCH_SPEC:
       case PNK_OBJECT_PROPERTY_NAME:
@@ -680,16 +681,17 @@ ParseNode::dump(int indent)
 
 void
 NullaryNode::dump()
 {
     switch (getKind()) {
       case PNK_TRUE:  fprintf(stderr, "#true");  break;
       case PNK_FALSE: fprintf(stderr, "#false"); break;
       case PNK_NULL:  fprintf(stderr, "#null");  break;
+      case PNK_RAW_UNDEFINED: fprintf(stderr, "#undefined"); break;
 
       case PNK_NUMBER: {
         ToCStringBuf cbuf;
         const char* cstr = NumberToCString(nullptr, &cbuf, pn_dval);
         if (!IsFinite(pn_dval))
             fputc('#', stderr);
         if (cstr)
             fprintf(stderr, "%s", cstr);
--- a/js/src/frontend/ParseNode.h
+++ b/js/src/frontend/ParseNode.h
@@ -49,16 +49,17 @@ class ObjectBox;
     F(TEMPLATE_STRING_LIST) \
     F(TEMPLATE_STRING) \
     F(TAGGED_TEMPLATE) \
     F(CALLSITEOBJ) \
     F(REGEXP) \
     F(TRUE) \
     F(FALSE) \
     F(NULL) \
+    F(RAW_UNDEFINED) \
     F(THIS) \
     F(FUNCTION) \
     F(MODULE) \
     F(IF) \
     F(SWITCH) \
     F(CASE) \
     F(WHILE) \
     F(DOWHILE) \
@@ -401,17 +402,18 @@ IsTypeofKind(ParseNodeKind kind)
  * PNK_TAGGED_TEMPLATE      pn_head: list of call, call site object, arg1, arg2, ... argN
  *              list        pn_count: 2 + N (N is the number of substitutions)
  * PNK_CALLSITEOBJ list     pn_head: a PNK_ARRAY node followed by
  *                          list of pn_count - 1 PNK_TEMPLATE_STRING nodes
  * PNK_REGEXP   nullary     pn_objbox: RegExp model object
  * PNK_NUMBER   dval        pn_dval: double value of numeric literal
  * PNK_TRUE,    nullary     pn_op: JSOp bytecode
  * PNK_FALSE,
- * PNK_NULL
+ * PNK_NULL,
+ * PNK_RAW_UNDEFINED
  *
  * PNK_THIS,        unary   pn_kid: '.this' Name if function `this`, else nullptr
  * PNK_SUPERBASE    unary   pn_kid: '.this' Name
  *
  * PNK_SETTHIS      binary  pn_left: '.this' Name, pn_right: SuperCall
  *
  * PNK_LEXICALSCOPE scope   pn_u.scope.bindings: scope bindings
  *                          pn_u.scope.body: scope body
@@ -681,17 +683,18 @@ class ParseNode
     }
 
     /* True if pn is a parsenode representing a literal constant. */
     bool isLiteral() const {
         return isKind(PNK_NUMBER) ||
                isKind(PNK_STRING) ||
                isKind(PNK_TRUE) ||
                isKind(PNK_FALSE) ||
-               isKind(PNK_NULL);
+               isKind(PNK_NULL) ||
+               isKind(PNK_RAW_UNDEFINED);
     }
 
     /* Return true if this node appears in a Directive Prologue. */
     bool isDirectivePrologueMember() const { return pn_prologue; }
 
     // True iff this is a for-in/of loop variable declaration (var/let/const).
     bool isForLoopDeclaration() const {
         if (isKind(PNK_VAR) || isKind(PNK_LET) || isKind(PNK_CONST)) {
@@ -1136,16 +1139,26 @@ class ThisLiteral : public UnaryNode
 };
 
 class NullLiteral : public ParseNode
 {
   public:
     explicit NullLiteral(const TokenPos& pos) : ParseNode(PNK_NULL, JSOP_NULL, PN_NULLARY, pos) { }
 };
 
+// This is only used internally, currently just for tagged templates.
+// It represents the value 'undefined' (aka `void 0`), like NullLiteral
+// represents the value 'null'.
+class RawUndefinedLiteral : public ParseNode
+{
+  public:
+    explicit RawUndefinedLiteral(const TokenPos& pos)
+      : ParseNode(PNK_RAW_UNDEFINED, JSOP_UNDEFINED, PN_NULLARY, pos) { }
+};
+
 class BooleanLiteral : public ParseNode
 {
   public:
     BooleanLiteral(bool b, const TokenPos& pos)
       : ParseNode(b ? PNK_TRUE : PNK_FALSE, b ? JSOP_TRUE : JSOP_FALSE, PN_NULLARY, pos)
     { }
 };
 
@@ -1356,16 +1369,17 @@ class ParseNodeAllocator
 inline bool
 ParseNode::isConstant()
 {
     switch (pn_type) {
       case PNK_NUMBER:
       case PNK_STRING:
       case PNK_TEMPLATE_STRING:
       case PNK_NULL:
+      case PNK_RAW_UNDEFINED:
       case PNK_FALSE:
       case PNK_TRUE:
         return true;
       case PNK_ARRAY:
       case PNK_OBJECT:
         MOZ_ASSERT(isOp(JSOP_NEWINIT));
         return !(pn_xflags & PNX_NONCONST);
       default:
--- a/js/src/frontend/Parser.cpp
+++ b/js/src/frontend/Parser.cpp
@@ -3080,30 +3080,30 @@ Parser<ParseHandler>::taggedTemplate(Yie
     handler.setEndPosition(nodeList, callSiteObjNode);
     return true;
 }
 
 template <typename ParseHandler>
 typename ParseHandler::Node
 Parser<ParseHandler>::templateLiteral(YieldHandling yieldHandling)
 {
-    Node pn = noSubstitutionTemplate();
+    Node pn = noSubstitutionUntaggedTemplate();
     if (!pn)
         return null();
 
     Node nodeList = handler.newList(PNK_TEMPLATE_STRING_LIST, pn);
     if (!nodeList)
         return null();
 
     TokenKind tt;
     do {
         if (!addExprAndGetNextTemplStrToken(yieldHandling, nodeList, &tt))
             return null();
 
-        pn = noSubstitutionTemplate();
+        pn = noSubstitutionUntaggedTemplate();
         if (!pn)
             return null();
 
         handler.addList(nodeList, pn);
     } while (tt == TOK_TEMPLATE_HEAD);
     return nodeList;
 }
 
@@ -3316,17 +3316,17 @@ Parser<ParseHandler>::innerFunction(Node
     return innerFunction(pn, outerpc, funbox, inHandling, yieldHandling, kind, inheritedDirectives,
                          newDirectives);
 }
 
 template <typename ParseHandler>
 bool
 Parser<ParseHandler>::appendToCallSiteObj(Node callSiteObj)
 {
-    Node cookedNode = noSubstitutionTemplate();
+    Node cookedNode = noSubstitutionTaggedTemplate();
     if (!cookedNode)
         return false;
 
     JSAtom* atom = tokenStream.getRawTemplateStringAtom();
     if (!atom)
         return false;
     Node rawNode = handler.newTemplateStringLiteral(atom, pos());
     if (!rawNode)
@@ -8706,18 +8706,33 @@ template <typename ParseHandler>
 typename ParseHandler::Node
 Parser<ParseHandler>::stringLiteral()
 {
     return handler.newStringLiteral(stopStringCompression(), pos());
 }
 
 template <typename ParseHandler>
 typename ParseHandler::Node
-Parser<ParseHandler>::noSubstitutionTemplate()
-{
+Parser<ParseHandler>::noSubstitutionTaggedTemplate()
+{
+    if (tokenStream.hasInvalidTemplateEscape()) {
+        tokenStream.clearInvalidTemplateEscape();
+        return handler.newRawUndefinedLiteral(pos());
+    }
+
+    return handler.newTemplateStringLiteral(stopStringCompression(), pos());
+}
+
+template <typename ParseHandler>
+typename ParseHandler::Node
+Parser<ParseHandler>::noSubstitutionUntaggedTemplate()
+{
+    if (!tokenStream.checkForInvalidTemplateEscapeError())
+        return null();
+
     return handler.newTemplateStringLiteral(stopStringCompression(), pos());
 }
 
 template <typename ParseHandler>
 JSAtom * Parser<ParseHandler>::stopStringCompression() {
     JSAtom* atom = tokenStream.currentToken().atom();
 
     // Large strings are fast to parse but slow to compress. Stop compression on
@@ -9420,17 +9435,17 @@ Parser<ParseHandler>::primaryExpr(YieldH
         handler.setEndPosition(expr, pos().end);
         return handler.parenthesize(expr);
       }
 
       case TOK_TEMPLATE_HEAD:
         return templateLiteral(yieldHandling);
 
       case TOK_NO_SUBS_TEMPLATE:
-        return noSubstitutionTemplate();
+        return noSubstitutionUntaggedTemplate();
 
       case TOK_STRING:
         return stringLiteral();
 
       case TOK_YIELD:
       case TOK_NAME: {
         if (tokenStream.currentName() == context->names().async &&
             !tokenStream.currentToken().nameContainsEscape())
--- a/js/src/frontend/Parser.h
+++ b/js/src/frontend/Parser.h
@@ -1048,17 +1048,18 @@ class Parser final : public ParserBase, 
     bool checkUnescapedName();
 
   private:
     Parser* thisForCtor() { return this; }
 
     JSAtom* stopStringCompression();
 
     Node stringLiteral();
-    Node noSubstitutionTemplate();
+    Node noSubstitutionTaggedTemplate();
+    Node noSubstitutionUntaggedTemplate();
     Node templateLiteral(YieldHandling yieldHandling);
     bool taggedTemplate(YieldHandling yieldHandling, Node nodeList, TokenKind tt);
     bool appendToCallSiteObj(Node callSiteObj);
     bool addExprAndGetNextTemplStrToken(YieldHandling yieldHandling, Node nodeList,
                                         TokenKind* ttp);
     bool checkStatementsEOF();
 
     inline Node newName(PropertyName* name);
--- a/js/src/frontend/SyntaxParseHandler.h
+++ b/js/src/frontend/SyntaxParseHandler.h
@@ -219,16 +219,17 @@ class SyntaxParseHandler
     Node newCallSiteObject(uint32_t begin) {
         return NodeGeneric;
     }
 
     void addToCallSiteObject(Node callSiteObj, Node rawNode, Node cookedNode) {}
 
     Node newThisLiteral(const TokenPos& pos, Node thisName) { return NodeGeneric; }
     Node newNullLiteral(const TokenPos& pos) { return NodeGeneric; }
+    Node newRawUndefinedLiteral(const TokenPos& pos) { return NodeGeneric; }
 
     template <class Boxer>
     Node newRegExp(RegExpObject* reobj, const TokenPos& pos, Boxer& boxer) { return NodeGeneric; }
 
     Node newConditional(Node cond, Node thenExpr, Node elseExpr) { return NodeGeneric; }
 
     Node newElision() { return NodeGeneric; }
 
--- a/js/src/frontend/TokenStream.cpp
+++ b/js/src/frontend/TokenStream.cpp
@@ -1904,66 +1904,16 @@ TokenStream::getTokenInternal(TokenKind*
     // immediately.
     userbuf.poison();
 #endif
     MOZ_MAKE_MEM_UNDEFINED(ttp, sizeof(*ttp));
     return false;
 }
 
 bool
-TokenStream::matchBracedUnicode(bool* matched, uint32_t* cp)
-{
-    int32_t c;
-    if (!peekChar(&c))
-        return false;
-    if (c != '{') {
-        *matched = false;
-        return true;
-    }
-
-    consumeKnownChar('{');
-
-    uint32_t start = userbuf.offset();
-
-    bool first = true;
-    uint32_t code = 0;
-    do {
-        int32_t c = getCharIgnoreEOL();
-        if (c == EOF) {
-            error(JSMSG_MALFORMED_ESCAPE, "Unicode");
-            return false;
-        }
-        if (c == '}') {
-            if (first) {
-                error(JSMSG_MALFORMED_ESCAPE, "Unicode");
-                return false;
-            }
-            break;
-        }
-
-        if (!JS7_ISHEX(c)) {
-            error(JSMSG_MALFORMED_ESCAPE, "Unicode");
-            return false;
-        }
-
-        code = (code << 4) | JS7_UNHEX(c);
-        if (code > unicode::NonBMPMax) {
-            errorAt(start, JSMSG_UNICODE_OVERFLOW, "escape sequence");
-            return false;
-        }
-
-        first = false;
-    } while (true);
-
-    *matched = true;
-    *cp = code;
-    return true;
-}
-
-bool
 TokenStream::getStringOrTemplateToken(int untilChar, Token** tp)
 {
     int c;
     int nc = -1;
 
     bool parsingTemplate = (untilChar == '`');
 
     *tp = newToken(-1);
@@ -1975,36 +1925,103 @@ TokenStream::getStringOrTemplateToken(in
     while ((c = getCharIgnoreEOL()) != untilChar) {
         if (c == EOF) {
             ungetCharIgnoreEOL(c);
             error(JSMSG_UNTERMINATED_STRING);
             return false;
         }
 
         if (c == '\\') {
+            // When parsing templates, we don't immediately report errors for
+            // invalid escapes; these are handled by the parser.
+            // In those cases we don't append to tokenbuf, since it won't be
+            // read.
             switch (c = getChar()) {
               case 'b': c = '\b'; break;
               case 'f': c = '\f'; break;
               case 'n': c = '\n'; break;
               case 'r': c = '\r'; break;
               case 't': c = '\t'; break;
               case 'v': c = '\v'; break;
 
               case '\n':
                 // ES5 7.8.4: an escaped line terminator represents
                 // no character.
                 continue;
 
               // Unicode character specification.
               case 'u': {
-                bool matched;
-                uint32_t code;
-                if (!matchBracedUnicode(&matched, &code))
+                uint32_t code = 0;
+
+                int32_t c2;
+                if (!peekChar(&c2))
                     return false;
-                if (matched) {
+
+                uint32_t start = userbuf.offset() - 2;
+
+                if (c2 == '{') {
+                    consumeKnownChar('{');
+
+                    bool first = true;
+                    bool valid = true;
+                    do {
+                        int32_t c = getCharIgnoreEOL();
+                        if (c == EOF) {
+                            if (parsingTemplate) {
+                                setInvalidTemplateEscape(start, InvalidEscapeType::Unicode);
+                                valid = false;
+                                break;
+                            }
+                            reportInvalidEscapeError(start, InvalidEscapeType::Unicode);
+                            return false;
+                        }
+                        if (c == '}') {
+                            if (first) {
+                                if (parsingTemplate) {
+                                    setInvalidTemplateEscape(start, InvalidEscapeType::Unicode);
+                                    valid = false;
+                                    break;
+                                }
+                                reportInvalidEscapeError(start, InvalidEscapeType::Unicode);
+                                return false;
+                            }
+                            break;
+                        }
+
+                        if (!JS7_ISHEX(c)) {
+                            if (parsingTemplate) {
+                                // We put the character back so that we read
+                                // it on the next pass, which matters if it
+                                // was '`' or '\'.
+                                ungetCharIgnoreEOL(c);
+                                setInvalidTemplateEscape(start, InvalidEscapeType::Unicode);
+                                valid = false;
+                                break;
+                            }
+                            reportInvalidEscapeError(start, InvalidEscapeType::Unicode);
+                            return false;
+                        }
+
+                        code = (code << 4) | JS7_UNHEX(c);
+                        if (code > unicode::NonBMPMax) {
+                            if (parsingTemplate) {
+                                setInvalidTemplateEscape(start + 3, InvalidEscapeType::UnicodeOverflow);
+                                valid = false;
+                                break;
+                            }
+                            reportInvalidEscapeError(start + 3, InvalidEscapeType::UnicodeOverflow);
+                            return false;
+                        }
+
+                        first = false;
+                    } while (true);
+
+                    if (!valid)
+                        continue;
+
                     MOZ_ASSERT(code <= unicode::NonBMPMax);
                     if (code < unicode::NonBMPMin) {
                         c = code;
                     } else {
                         if (!tokenbuf.append(unicode::LeadSurrogate(code)))
                             return false;
                         c = unicode::TrailSurrogate(code);
                     }
@@ -2016,48 +2033,57 @@ TokenStream::getStringOrTemplateToken(in
                     JS7_ISHEX(cp[0]) && JS7_ISHEX(cp[1]) && JS7_ISHEX(cp[2]) && JS7_ISHEX(cp[3]))
                 {
                     c = JS7_UNHEX(cp[0]);
                     c = (c << 4) + JS7_UNHEX(cp[1]);
                     c = (c << 4) + JS7_UNHEX(cp[2]);
                     c = (c << 4) + JS7_UNHEX(cp[3]);
                     skipChars(4);
                 } else {
-                    error(JSMSG_MALFORMED_ESCAPE, "Unicode");
+                    if (parsingTemplate) {
+                        setInvalidTemplateEscape(start, InvalidEscapeType::Unicode);
+                        continue;
+                    }
+                    reportInvalidEscapeError(start, InvalidEscapeType::Unicode);
                     return false;
                 }
                 break;
               }
 
               // Hexadecimal character specification.
               case 'x': {
                 char16_t cp[2];
                 if (peekChars(2, cp) && JS7_ISHEX(cp[0]) && JS7_ISHEX(cp[1])) {
                     c = (JS7_UNHEX(cp[0]) << 4) + JS7_UNHEX(cp[1]);
                     skipChars(2);
                 } else {
-                    error(JSMSG_MALFORMED_ESCAPE, "hexadecimal");
+                    uint32_t start = userbuf.offset() - 2;
+                    if (parsingTemplate) {
+                        setInvalidTemplateEscape(start, InvalidEscapeType::Hexadecimal);
+                        continue;
+                    }
+                    reportInvalidEscapeError(start, InvalidEscapeType::Hexadecimal);
                     return false;
                 }
                 break;
               }
 
               default:
                 // Octal character specification.
                 if (JS7_ISOCT(c)) {
                     int32_t val = JS7_UNOCT(c);
 
                     if (!peekChar(&c))
                         return false;
 
                     // Strict mode code allows only \0, then a non-digit.
                     if (val != 0 || JS7_ISDEC(c)) {
                         if (parsingTemplate) {
-                            error(JSMSG_DEPRECATED_OCTAL);
-                            return false;
+                            setInvalidTemplateEscape(userbuf.offset() - 2, InvalidEscapeType::Octal);
+                            continue;
                         }
                         if (!reportStrictModeError(JSMSG_DEPRECATED_OCTAL))
                             return false;
                         flags.sawOctalEscape = true;
                     }
 
                     if (JS7_ISOCT(c)) {
                         val = 8 * val + JS7_UNOCT(c);
--- a/js/src/frontend/TokenStream.h
+++ b/js/src/frontend/TokenStream.h
@@ -75,16 +75,30 @@ struct TokenPos {
 
     bool encloses(const TokenPos& pos) const {
         return begin <= pos.begin && pos.end <= end;
     }
 };
 
 enum DecimalPoint { NoDecimal = false, HasDecimal = true };
 
+enum class InvalidEscapeType {
+    // No invalid character escapes.
+    None,
+    // A malformed \x escape.
+    Hexadecimal,
+    // A malformed \u escape.
+    Unicode,
+    // An otherwise well-formed \u escape which represents a
+    // codepoint > 10FFFF.
+    UnicodeOverflow,
+    // An octal escape in a template token.
+    Octal
+};
+
 class TokenStream;
 
 struct Token
 {
   private:
     // Sometimes the parser needs to inform the tokenizer to interpret
     // subsequent text in a particular manner: for example, to tokenize a
     // keyword as an identifier, not as the actual keyword, on the right-hand
@@ -356,16 +370,33 @@ class MOZ_STACK_CLASS TokenStream
     }
 
     // Flag methods.
     bool isEOF() const { return flags.isEOF; }
     bool sawOctalEscape() const { return flags.sawOctalEscape; }
     bool hadError() const { return flags.hadError; }
     void clearSawOctalEscape() { flags.sawOctalEscape = false; }
 
+    bool hasInvalidTemplateEscape() const {
+        return invalidTemplateEscapeType != InvalidEscapeType::None;
+    }
+    void clearInvalidTemplateEscape() {
+        invalidTemplateEscapeType = InvalidEscapeType::None;
+    }
+
+    // If there is an invalid escape in a template, report it and return false,
+    // otherwise return true.
+    bool checkForInvalidTemplateEscapeError() {
+        if (invalidTemplateEscapeType == InvalidEscapeType::None)
+            return true;
+
+        reportInvalidEscapeError(invalidTemplateEscapeOffset, invalidTemplateEscapeType);
+        return false;
+    }
+
     // TokenStream-specific error reporters.
     bool reportError(unsigned errorNumber, ...);
     bool reportErrorNoOffset(unsigned errorNumber, ...);
 
     // Report the given error at the current offset.
     void error(unsigned errorNumber, ...);
 
     // Report the given error at the given offset.
@@ -417,16 +448,43 @@ class MOZ_STACK_CLASS TokenStream
     }
 
   private:
     // These are private because they should only be called by the tokenizer
     // while tokenizing not by, for example, BytecodeEmitter.
     bool reportStrictModeError(unsigned errorNumber, ...);
     bool strictMode() const { return strictModeGetter && strictModeGetter->strictMode(); }
 
+    void setInvalidTemplateEscape(uint32_t offset, InvalidEscapeType type) {
+        MOZ_ASSERT(type != InvalidEscapeType::None);
+        if (invalidTemplateEscapeType != InvalidEscapeType::None)
+            return;
+        invalidTemplateEscapeOffset = offset;
+        invalidTemplateEscapeType = type;
+    }
+    void reportInvalidEscapeError(uint32_t offset, InvalidEscapeType type) {
+        switch (type) {
+            case InvalidEscapeType::None:
+                MOZ_ASSERT_UNREACHABLE("unexpected InvalidEscapeType");
+                return;
+            case InvalidEscapeType::Hexadecimal:
+                errorAt(offset, JSMSG_MALFORMED_ESCAPE, "hexadecimal");
+                return;
+            case InvalidEscapeType::Unicode:
+                errorAt(offset, JSMSG_MALFORMED_ESCAPE, "Unicode");
+                return;
+            case InvalidEscapeType::UnicodeOverflow:
+                errorAt(offset, JSMSG_UNICODE_OVERFLOW, "escape sequence");
+                return;
+            case InvalidEscapeType::Octal:
+                errorAt(offset, JSMSG_DEPRECATED_OCTAL);
+                return;
+        }
+    }
+
     static JSAtom* atomize(ExclusiveContext* cx, CharBuffer& cb);
     MOZ_MUST_USE bool putIdentInTokenbuf(const char16_t* identStart);
 
     struct Flags
     {
         bool isEOF:1;           // Hit end of file.
         bool isDirtyLine:1;     // Non-whitespace since start of line.
         bool sawOctalEscape:1;  // Saw an octal character escape.
@@ -437,16 +495,19 @@ class MOZ_STACK_CLASS TokenStream
         Flags()
           : isEOF(), isDirtyLine(), sawOctalEscape(), hadError(), hitOOM()
         {}
     };
 
     bool awaitIsKeyword = false;
     friend class AutoAwaitIsKeyword;
 
+    uint32_t invalidTemplateEscapeOffset = 0;
+    InvalidEscapeType invalidTemplateEscapeType = InvalidEscapeType::None;
+
   public:
     typedef Token::Modifier Modifier;
     static constexpr Modifier None = Token::None;
     static constexpr Modifier Operand = Token::Operand;
     static constexpr Modifier KeywordIsName = Token::KeywordIsName;
     static constexpr Modifier TemplateTail = Token::TemplateTail;
 
     typedef Token::ModifierException ModifierException;
@@ -950,17 +1011,16 @@ class MOZ_STACK_CLASS TokenStream
         const char16_t* base_;          // base of buffer
         uint32_t startOffset_;          // offset of base_[0]
         const char16_t* limit_;         // limit for quick bounds check
         const char16_t* ptr;            // next char to get
     };
 
     MOZ_MUST_USE bool getTokenInternal(TokenKind* ttp, Modifier modifier);
 
-    MOZ_MUST_USE bool matchBracedUnicode(bool* matched, uint32_t* code);
     MOZ_MUST_USE bool getStringOrTemplateToken(int untilChar, Token** tp);
 
     int32_t getChar();
     int32_t getCharIgnoreEOL();
     void ungetChar(int32_t c);
     void ungetCharIgnoreEOL(int32_t c);
     Token* newToken(ptrdiff_t adjust);
     uint32_t peekUnicodeEscape(uint32_t* codePoint);
--- a/js/src/tests/ecma_6/TemplateStrings/tagTempl.js
+++ b/js/src/tests/ecma_6/TemplateStrings/tagTempl.js
@@ -282,10 +282,182 @@ assertEq(func`hey``there``amine`, "was n
 assertEq(func`hey``tshere``amine`, "was not there");
 assertEq(func`heys``there``mine`, "was not hey");
 
 // String.raw
 assertEq(String.raw`h\r\ney${4}there\n`, "h\\r\\ney4there\\n");
 assertEq(String.raw`hey`, "hey");
 assertEq(String.raw``, "");
 
+// Invalid escape sequences
+check(raw`\01`, ["\\01"]);
+check(raw`\01${0}right`, ["\\01","right"]);
+check(raw`left${0}\01`, ["left","\\01"]);
+check(raw`left${0}\01${1}right`, ["left","\\01","right"]);
+check(raw`\1`, ["\\1"]);
+check(raw`\1${0}right`, ["\\1","right"]);
+check(raw`left${0}\1`, ["left","\\1"]);
+check(raw`left${0}\1${1}right`, ["left","\\1","right"]);
+check(raw`\xg`, ["\\xg"]);
+check(raw`\xg${0}right`, ["\\xg","right"]);
+check(raw`left${0}\xg`, ["left","\\xg"]);
+check(raw`left${0}\xg${1}right`, ["left","\\xg","right"]);
+check(raw`\xAg`, ["\\xAg"]);
+check(raw`\xAg${0}right`, ["\\xAg","right"]);
+check(raw`left${0}\xAg`, ["left","\\xAg"]);
+check(raw`left${0}\xAg${1}right`, ["left","\\xAg","right"]);
+check(raw`\u0`, ["\\u0"]);
+check(raw`\u0${0}right`, ["\\u0","right"]);
+check(raw`left${0}\u0`, ["left","\\u0"]);
+check(raw`left${0}\u0${1}right`, ["left","\\u0","right"]);
+check(raw`\u0g`, ["\\u0g"]);
+check(raw`\u0g${0}right`, ["\\u0g","right"]);
+check(raw`left${0}\u0g`, ["left","\\u0g"]);
+check(raw`left${0}\u0g${1}right`, ["left","\\u0g","right"]);
+check(raw`\u00g`, ["\\u00g"]);
+check(raw`\u00g${0}right`, ["\\u00g","right"]);
+check(raw`left${0}\u00g`, ["left","\\u00g"]);
+check(raw`left${0}\u00g${1}right`, ["left","\\u00g","right"]);
+check(raw`\u000g`, ["\\u000g"]);
+check(raw`\u000g${0}right`, ["\\u000g","right"]);
+check(raw`left${0}\u000g`, ["left","\\u000g"]);
+check(raw`left${0}\u000g${1}right`, ["left","\\u000g","right"]);
+check(raw`\u{}`, ["\\u{}"]);
+check(raw`\u{}${0}right`, ["\\u{}","right"]);
+check(raw`left${0}\u{}`, ["left","\\u{}"]);
+check(raw`left${0}\u{}${1}right`, ["left","\\u{}","right"]);
+check(raw`\u{-0}`, ["\\u{-0}"]);
+check(raw`\u{-0}${0}right`, ["\\u{-0}","right"]);
+check(raw`left${0}\u{-0}`, ["left","\\u{-0}"]);
+check(raw`left${0}\u{-0}${1}right`, ["left","\\u{-0}","right"]);
+check(raw`\u{g}`, ["\\u{g}"]);
+check(raw`\u{g}${0}right`, ["\\u{g}","right"]);
+check(raw`left${0}\u{g}`, ["left","\\u{g}"]);
+check(raw`left${0}\u{g}${1}right`, ["left","\\u{g}","right"]);
+check(raw`\u{0`, ["\\u{0"]);
+check(raw`\u{0${0}right`, ["\\u{0","right"]);
+check(raw`left${0}\u{0`, ["left","\\u{0"]);
+check(raw`left${0}\u{0${1}right`, ["left","\\u{0","right"]);
+check(raw`\u{\u{0}`, ["\\u{\\u{0}"]);
+check(raw`\u{\u{0}${0}right`, ["\\u{\\u{0}","right"]);
+check(raw`left${0}\u{\u{0}`, ["left","\\u{\\u{0}"]);
+check(raw`left${0}\u{\u{0}${1}right`, ["left","\\u{\\u{0}","right"]);
+check(raw`\u{110000}`, ["\\u{110000}"]);
+check(raw`\u{110000}${0}right`, ["\\u{110000}","right"]);
+check(raw`left${0}\u{110000}`, ["left","\\u{110000}"]);
+check(raw`left${0}\u{110000}${1}right`, ["left","\\u{110000}","right"]);
+
+check(cooked`\01`, [void 0]);
+check(cooked`\01${0}right`, [void 0,"right"]);
+check(cooked`left${0}\01`, ["left",void 0]);
+check(cooked`left${0}\01${1}right`, ["left",void 0,"right"]);
+check(cooked`\1`, [void 0]);
+check(cooked`\1${0}right`, [void 0,"right"]);
+check(cooked`left${0}\1`, ["left",void 0]);
+check(cooked`left${0}\1${1}right`, ["left",void 0,"right"]);
+check(cooked`\xg`, [void 0]);
+check(cooked`\xg${0}right`, [void 0,"right"]);
+check(cooked`left${0}\xg`, ["left",void 0]);
+check(cooked`left${0}\xg${1}right`, ["left",void 0,"right"]);
+check(cooked`\xAg`, [void 0]);
+check(cooked`\xAg${0}right`, [void 0,"right"]);
+check(cooked`left${0}\xAg`, ["left",void 0]);
+check(cooked`left${0}\xAg${1}right`, ["left",void 0,"right"]);
+check(cooked`\u0`, [void 0]);
+check(cooked`\u0${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u0`, ["left",void 0]);
+check(cooked`left${0}\u0${1}right`, ["left",void 0,"right"]);
+check(cooked`\u0g`, [void 0]);
+check(cooked`\u0g${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u0g`, ["left",void 0]);
+check(cooked`left${0}\u0g${1}right`, ["left",void 0,"right"]);
+check(cooked`\u00g`, [void 0]);
+check(cooked`\u00g${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u00g`, ["left",void 0]);
+check(cooked`left${0}\u00g${1}right`, ["left",void 0,"right"]);
+check(cooked`\u000g`, [void 0]);
+check(cooked`\u000g${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u000g`, ["left",void 0]);
+check(cooked`left${0}\u000g${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{}`, [void 0]);
+check(cooked`\u{}${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{}`, ["left",void 0]);
+check(cooked`left${0}\u{}${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{-0}`, [void 0]);
+check(cooked`\u{-0}${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{-0}`, ["left",void 0]);
+check(cooked`left${0}\u{-0}${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{g}`, [void 0]);
+check(cooked`\u{g}${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{g}`, ["left",void 0]);
+check(cooked`left${0}\u{g}${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{0`, [void 0]);
+check(cooked`\u{0${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{0`, ["left",void 0]);
+check(cooked`left${0}\u{0${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{\u{0}`, [void 0]);
+check(cooked`\u{\u{0}${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{\u{0}`, ["left",void 0]);
+check(cooked`left${0}\u{\u{0}${1}right`, ["left",void 0,"right"]);
+check(cooked`\u{110000}`, [void 0]);
+check(cooked`\u{110000}${0}right`, [void 0,"right"]);
+check(cooked`left${0}\u{110000}`, ["left",void 0]);
+check(cooked`left${0}\u{110000}${1}right`, ["left",void 0,"right"]);
+
+syntaxError("`\\01`");
+syntaxError("`\\01${0}right`");
+syntaxError("`left${0}\\01`");
+syntaxError("`left${0}\\01${1}right`");
+syntaxError("`\\1`");
+syntaxError("`\\1${0}right`");
+syntaxError("`left${0}\\1`");
+syntaxError("`left${0}\\1${1}right`");
+syntaxError("`\\xg`");
+syntaxError("`\\xg${0}right`");
+syntaxError("`left${0}\\xg`");
+syntaxError("`left${0}\\xg${1}right`");
+syntaxError("`\\xAg`");
+syntaxError("`\\xAg${0}right`");
+syntaxError("`left${0}\\xAg`");
+syntaxError("`left${0}\\xAg${1}right`");
+syntaxError("`\\u0`");
+syntaxError("`\\u0${0}right`");
+syntaxError("`left${0}\\u0`");
+syntaxError("`left${0}\\u0${1}right`");
+syntaxError("`\\u0g`");
+syntaxError("`\\u0g${0}right`");
+syntaxError("`left${0}\\u0g`");
+syntaxError("`left${0}\\u0g${1}right`");
+syntaxError("`\\u00g`");
+syntaxError("`\\u00g${0}right`");
+syntaxError("`left${0}\\u00g`");
+syntaxError("`left${0}\\u00g${1}right`");
+syntaxError("`\\u000g`");
+syntaxError("`\\u000g${0}right`");
+syntaxError("`left${0}\\u000g`");
+syntaxError("`left${0}\\u000g${1}right`");
+syntaxError("`\\u{}`");
+syntaxError("`\\u{}${0}right`");
+syntaxError("`left${0}\\u{}`");
+syntaxError("`left${0}\\u{}${1}right`");
+syntaxError("`\\u{-0}`");
+syntaxError("`\\u{-0}${0}right`");
+syntaxError("`left${0}\\u{-0}`");
+syntaxError("`left${0}\\u{-0}${1}right`");
+syntaxError("`\\u{g}`");
+syntaxError("`\\u{g}${0}right`");
+syntaxError("`left${0}\\u{g}`");
+syntaxError("`left${0}\\u{g}${1}right`");
+syntaxError("`\\u{0`");
+syntaxError("`\\u{0${0}right`");
+syntaxError("`left${0}\\u{0`");
+syntaxError("`left${0}\\u{0${1}right`");
+syntaxError("`\\u{\\u{0}`");
+syntaxError("`\\u{\\u{0}${0}right`");
+syntaxError("`left${0}\\u{\\u{0}`");
+syntaxError("`left${0}\\u{\\u{0}${1}right`");
+syntaxError("`\\u{110000}`");
+syntaxError("`\\u{110000}${0}right`");
+syntaxError("`left${0}\\u{110000}`");
+syntaxError("`left${0}\\u{110000}${1}right`");
+
 
 reportCompare(0, 0, "ok");
--- a/js/src/tests/js1_8_5/reflect-parse/templateStrings.js
+++ b/js/src/tests/js1_8_5/reflect-parse/templateStrings.js
@@ -2,16 +2,18 @@
 function test() {
 
 // template strings
 assertStringExpr("`hey there`", literal("hey there"));
 assertStringExpr("`hey\nthere`", literal("hey\nthere"));
 assertExpr("`hey${\"there\"}`", templateLit([lit("hey"), lit("there"), lit("")]));
 assertExpr("`hey${\"there\"}mine`", templateLit([lit("hey"), lit("there"), lit("mine")]));
 assertExpr("`hey${a == 5}mine`", templateLit([lit("hey"), binExpr("==", ident("a"), lit(5)), lit("mine")]));
+assertExpr("func`hey\\x`", taggedTemplate(ident("func"), template(["hey\\x"], [void 0])));
+assertExpr("func`hey${4}\\x`", taggedTemplate(ident("func"), template(["hey","\\x"], ["hey",void 0], lit(4))));
 assertExpr("`hey${`there${\"how\"}`}mine`", templateLit([lit("hey"),
            templateLit([lit("there"), lit("how"), lit("")]), lit("mine")]));
 assertExpr("func`hey`", taggedTemplate(ident("func"), template(["hey"], ["hey"])));
 assertExpr("func`hey${\"4\"}there`", taggedTemplate(ident("func"),
            template(["hey", "there"], ["hey", "there"], lit("4"))));
 assertExpr("func`hey${\"4\"}there${5}`", taggedTemplate(ident("func"),
            template(["hey", "there", ""], ["hey", "there", ""],
                   lit("4"), lit(5))));