Closed Bug 1024748 Opened 5 years ago Closed 5 years ago

Implement Template Literals as described in ES6 draft section 12.2.9

Categories

(Core :: JavaScript Engine, defect)

defect
Not set

Tracking

()

RESOLVED FIXED
mozilla33

People

(Reporter: gupta.rajagopal, Assigned: gupta.rajagopal)

References

()

Details

(Keywords: dev-doc-complete, feature, Whiteboard: [DocArea=JS])

Attachments

(2 files, 11 obsolete files)

42.43 KB, patch
gupta.rajagopal
: review+
Details | Diff | Splinter Review
5.39 KB, patch
gupta.rajagopal
: review+
Details | Diff | Splinter Review
User Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:30.0) Gecko/20100101 Firefox/30.0 (Beta/Release)
Build ID: 20140605174243
Blocks: 688857
Keywords: feature
OS: Mac OS X → All
Hardware: x86 → All
Attached patch Original patch v1 (obsolete) — Splinter Review
Attachment #8439544 - Flags: review?(jorendorff)
The JSOP_ADD instruction is not quite correct, because it does not apply the correct string conversion. Is there already a matching opcode for string conversion you can use here?

Test case: `${{valueOf: () => "<valueOf>", toString: () => "<toString>"}}`
Expected: Evaluates to "<toString>"
I'm adding this part again because I left some tests commented out last time
Attachment #8439544 - Attachment is obsolete: true
Attachment #8439544 - Flags: review?(jorendorff)
Attached patch Added JSOP_TOSTRING v1 (obsolete) — Splinter Review
Added the JSOP_TOSTRING bytecode
Attachment #8440003 - Flags: review?(jorendorff)
Attachment #8440002 - Flags: review?(jorendorff)
Attached patch Added JSOP_TOSTRING v2 (obsolete) — Splinter Review
Attachment #8440003 - Attachment is obsolete: true
Attachment #8440003 - Flags: review?(jorendorff)
Attachment #8440012 - Flags: review?(jorendorff)
Comment on attachment 8440002 [details] [diff] [review]
The one where toString behavior is incorrect version 1

Good work. Lots of comments. Ping me when you get a revised patch posted.

Please generate patches for review using diff -U8. The extra context
really helps reviewers.

Clearing r? for now.

In js/src/frontend/BytecodeEmitter.cpp:
> /* A function, so that we avoid macro-bloating all the other callsites. */
>+/* This function updates the line number and column number information in the source notes */
> static bool
> UpdateSourceCoordNotes(ExclusiveContext *cx, BytecodeEmitter *bce, uint32_t offset)

Please don't write two adjacent comments like that. In this case, the
pre-existing comment can just be deleted. Your comment is a sentence, so
it should end with a period, but "This function" can be removed:

    /* Update the line number and column number information in the source notes. */

In EmitTemplateString:
>+            // There are two items on the stack. We should push in add now

This comment isn't quite grammatical. How about:

    // We've pushed two items onto the stack. Add them together, leaving just one.

In js/src/frontend/FullParseHandler.h:
>+    ParseNode *newTemplateStringLiteral(JSAtom *atom, const TokenPos &pos) {
>+        return new_<NullaryNode>(PNK_TEMPLATE_STRING, JSOP_STRING, pos, atom);
>+    }

Please use JSOP_NOP instead of JSOP_STRING.

The reason for this is a little embarrassing. The pn_op field of
ParseNodes is *mostly* unused and we'd like to remove it. So please
don't store any useful information there if you can help it. Thanks. :)

In js/src/frontend/Parser.cpp:
>+typename ParseHandler::Node
>+Parser<ParseHandler>::templateStringExpr()

Please rename this method to templateLiteral (the name ES6 uses for this).

This code is pretty nice! It can be simpler, though. Ideally the loop
should look something like this (pseudocode):

    do {
        parse an expression;
        parse a TemplateTail;
    } while (tt == TOK_TEMPLATE_HEAD);

That is: you should not need to call expr() both outside the loop and
inside the loop.

>+Parser<ParseHandler>::templateStringLiteral()

If you keep this method, please rename it to noSubstitutionTemplate (the
name the ES6 specification uses for this).

>+    // Large strings are fast to parse but slow to compress. Stop compression on
>+    // them, so we don't wait for a long time for compression to finish at the
>+    // end of compilation.
>+    const size_t HUGE_STRING = 50000;
>+    if (sct && sct->active() && atom->length() >= HUGE_STRING)
>+        sct->abort();

Please never copy and paste code if there is a reasonable alternative!
Common this up with stringLiteral(), either by factoring out a method
containing the shared code, or by merging them completely and adding an
extra argument to tell if it's a template literal or not.

In frontend/SyntaxParseHandler.h:
>+#ifdef JS_HAS_TEMPLATE_STRINGS
>+    Node newTemplateStringLiteral(JSAtom *atom, const TokenPos &pos) {
>+        lastAtom = atom;
>+        lastStringPos = pos;
>+        return NodeString;
>+    }
>+#endif

This will cause isStringExprStatement to return true for statements like

    `use strict`;

which we don't want, I think. See where this is called and what it is
used for, and find out if it's a bug. If so, figure out how to write a
test.

In frontend/TokenStream.h:
>+    TOK_TEMPLATE_STRING_SUBS,      // template string with substitutions
>+    TOK_TEMPLATE_STRING_NO_SUBS,   // template string without substitutions

Please rename these to
    TOK_TEMPLATE_HEAD,             // start of template literal with substitutions
    TOK_NO_SUBS_TEMPLATE,          // template literal without substitutions

In js/src/frontend/TokenStream.cpp:
>+    // Check if in the middle of a template string. Have to get this out of
>+    // the way first.
>+#ifdef JS_HAS_TEMPLATE_STRINGS
>+    if (MOZ_UNLIKELY(modifier == TemplateTail)) {
>+        c = '`';
>+        c1kind = TemplateString;
>+        goto template_string_cont;
>+    }
>+#endif

Instead of a goto, is it possible to factor that code out into a
separate function that we can call from here and from the string-parsing
code?

I'm not allergic to goto, so if it's even uglier to implement without
goto, just say so. :)

>+            if (c == '$' && nc == '{') {
>+                tp->type = TOK_TEMPLATE_STRING_SUBS;
>+            } else {
>+                tp->type = TOK_TEMPLATE_STRING_NO_SUBS;
>+            }

Style nit: Please remove the curly braces here, since the `if`,
then-substatement, and else-substatement all fit on single lines.

In js/src/jsreflect.cpp:
>+#ifdef JS_HAS_TEMPLATE_STRINGS
>+      case PNK_TEMPLATE_STRING:
>+#endif

OK, but what about substitutions? We need a test for Reflect.parse that
uses substitutions, and that means some code here to implement.

Let me know if I should explain this a little more.

Esprima's "harmony" branch is presumably ahead of us on this. Take a
look at what they've done, maybe?

In js/src/tests/ecma_6/TemplateStrings/templLitTests.js:
>+syntaxError("${}");

Please add these tests. (I haven't tested these tests, so there may be
bugs. Sorry!)

syntaxError("${");
syntaxError("${\\n}");
syntaxError("${yield 0}");

// Extra whitespace inside a template substitution is ignored.
assertEq(`${
0
}`, "0");

assertEq(`${ // Comments work in template substitutions.
// Even comments that look like code:
// 0}`, "FAIL");   /* NOTE: This whole line is a comment.
0}`, "0");

// Template substitutions are expressions, not statements.
syntaxError("${0;}");
assertEq(`${{}}`, "[object Object]");
assertEq(`${
    function f() {
        return "ok";
    }()
}`, "ok");

// Template substitutions can have side effects.
var x = 0;
assertEq(`= ${x += 1}`, "= 1");
assertEq(x, 1);

// The production for a template substitution is Expression, not
// AssignmentExpression.
x = 0;
assertEq(`${++x, "o"}k`, "ok");
assertEq(x, 1);

// --> is not a comment inside a template.
assertEq(`
--> this is text
`, "\n--> this is text\n");

// reentrancy
function f(n) {
    if (n === 0)
        return "";
    return `${n}${f(n - 1)}`;
}
assertEq(f(9), "987654321");

// Template string substitutions in generator functions can yield.
function* g() {
    return `${yield 1} ${yield 2}`;
}
var it = g();
var next = it.next();
assertEq(next.done, false);
assertEq(next.value, 1);
next = it.next("hello");
assertEq(next.done, false);
assertEq(next.value, 2);
next = it.next("world");
assertEq(next.done, true);
assertEq(next.value, "hello world");

// undefined
assertEq(`${void 0}`, "undefined");
assertEq(`${Object.doesNotHaveThisProperty}`, "undefined");

// Template string substitution values are converted to strings using ToString.
assertEq(`${{toString: () => "PASS"}}`, "PASS");
assertEq(`${{toString: () => "PASS", valueOf: () => "FAIL"}}`, "PASS");
Attachment #8440002 - Flags: review?(jorendorff)
Comment on attachment 8440012 [details] [diff] [review]
Added JSOP_TOSTRING v2

Review of attachment 8440012 [details] [diff] [review]:
-----------------------------------------------------------------

Very nice. r=me with the one comment addressed.

Until you have level 3 access, this means you need to post an updated patch. But you can just set 'review+' on the new patch. There's no need for another full review.

::: js/src/frontend/BytecodeEmitter.cpp
@@ +3452,2 @@
>          if (pn2 != pn->pn_head) {
>              // There are two items on the stack. We should push in add now

Now this comment can be changed to say "two strings" rather than "two items".

::: js/src/tests/ecma_6/TemplateStrings/templLitTests.js
@@ +45,5 @@
>  // inception
>  assertEq("abcdef", `a${`b${"cd"}e${"f"}`}`);
>  
> +// proper toString behavior
> +assertEq("<toString>", `${{valueOf: () => "<valueOf>", toString: () => "<toString>"}}`);

Ah! Never mind the very similar test I wrote for this, then (in the other review).

::: js/src/vm/Interpreter.cpp
@@ +2735,5 @@
> +        operString = ToString<CanGC>(cx, oper);
> +        if (!operString)
> +            return false;
> +    }
> +    oper.setString(operString);

Shorter:

    if (!oper.isString()) {
        JSString *operString = ToString<CanGC>(cx, oper);
        if (!operString)
            goto error;
        oper.setString(operString);
    }

Note that `return false` instead of `goto error` would bypass the code that looks to see if we're in a `try` block. Add a test that would catch this bug.
Attachment #8440012 - Flags: review?(jorendorff) → review+
Attachment #8440002 - Attachment is obsolete: true
Attachment #8442258 - Flags: review?(jorendorff)
Attachment #8440012 - Attachment is obsolete: true
Attachment #8442404 - Flags: review+
Comment on attachment 8442258 [details] [diff] [review]
The one where toString behavior is incorrect version 2 (addressed review comments)

Review of attachment 8442258 [details] [diff] [review]:
-----------------------------------------------------------------

OK, r=me with these comments addressed. Please update your patch, post it here, and push it to the Try Server.

When the Try Server likes it, you can post the link here and add checkin-needed to the Keywords field.

::: js/src/frontend/SyntaxParseHandler.h
@@ +78,5 @@
> +#ifdef JS_HAS_TEMPLATE_STRINGS
> +    Node newTemplateStringLiteral(JSAtom *atom, const TokenPos &pos) {
> +        lastAtom = atom;
> +        lastStringPos = pos;
> +        return NodeGeneric;

Don't set lastAtom or lastStringPos, since you're not telling the caller that this is a string.

::: js/src/frontend/TokenStream.cpp
@@ +1056,5 @@
> +#ifdef JS_HAS_TEMPLATE_STRINGS
> +    if (MOZ_UNLIKELY(modifier == TemplateTail)) {
> +        if (getStringOrTemplateToken('`', TemplateString, &tp) == true)
> +            goto out;
> +        goto error;

Style nit: Instead, write:

    if (!getStringOrTemplateToken('`', TemplateString, &tp))
        goto error;
    goto out;

This is more like how every other error path in the codebase is written, and if we ever need to add more code between the if statement and 'goto out', it can be done without rearranging anything.

@@ +1250,5 @@
>  #endif
>           ) {
> +        if (getStringOrTemplateToken(c, c1kind, &tp) == true)
> +            goto out;
> +        goto error;

Same here.

@@ +1583,5 @@
>      onError();
>      return TOK_ERROR;
>  }
>  
> +bool TokenStream::getStringOrTemplateToken(int c, FirstCharKind c1kind, Token **tp)

Try replacing
    c1kind == TemplateString        with     qc == '`'
    c1kind == String                with     qc != '`'

That way the c1kind argument can be deleted, and FirstCharKind does not need to be moved to the header. Also I think you can get rid of TemplateString and just use String for '`'.

@@ +1586,5 @@
>  
> +bool TokenStream::getStringOrTemplateToken(int c, FirstCharKind c1kind, Token **tp)
> +{
> +    *tp = newToken(-1);
> +    int qc = c;

The argument should be named qc, and just declare `int c;` here.

::: js/src/tests/js1_8_5/extensions/reflect-parse.js
@@ +404,5 @@
>  var hasTemplateStrings = false;  try { eval("``"); hasTemplateStrings = true; } catch (exc) { }
>  if (hasTemplateStrings == true) {
>      assertStringExpr("`hey there`", literal("hey there"));
>      assertStringExpr("`hey\nthere`", literal("hey\nthere"));
> +    assertExpr("`hey${\"there\"}`", templateLit([lit("hey"), lit("there"), lit("")]));

OK! This will be fine for now. Before we ship it, we need to settle on a final form of Reflect.parse output that also supports tagged templates and that we can share with Esprima. Here's what Esprima does right now:

js> load("esprima.js");
js> esprima.parse("`hey${\"there\"}`").body[0].expression
({
    type: "TemplateLiteral",
    quasis: [
        {type: "TemplateElement", value: {raw: "hey", cooked: "hey"}, tail: false},
        {type: "TemplateElement", value: {raw: "", cooked: ""}, tail: true}
    ],
    expressions: [
        {type: "Literal", value: "there", raw: "\"there\""}
    ]
})

Somewhere between what you've got and what they've got is where we need to end up...
Attachment #8442258 - Flags: review?(jorendorff) → review+
Attachment #8442258 - Attachment is obsolete: true
Attachment #8442438 - Flags: review+
Keywords: checkin-needed
Assignee: nobody → gupta.rajagopal
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Keywords: checkin-needed
Duplicate of this bug: 1026027
Can you please suggest some commit messages for these patches per the link below? I'm not sure I understand them well enough to do so myself. Also note that your patches should contain the other commit information as well. Thanks!

https://developer.mozilla.org/en-US/docs/Developer_Guide/Committing_Rules_and_Responsibilities#Checkin_comment

https://developer.mozilla.org/en-US/docs/Mercurial_FAQ#How_can_I_generate_a_patch_for_somebody_else_to_check-in_for_me.3F
Keywords: checkin-needed
Attachment #8442438 - Attachment is obsolete: true
Attachment #8444509 - Flags: review+
Attachment #8442404 - Attachment is obsolete: true
Attachment #8444510 - Flags: review+
Ryan,

I've updated the patches with commit messages. Please let me know if anything else is needed, thanks!
Attachment #8444509 - Attachment is obsolete: true
Attachment #8444524 - Flags: review+
Attachment #8444510 - Attachment is obsolete: true
Attachment #8444526 - Flags: review+
Attachment #8444526 - Attachment is obsolete: true
Attachment #8444576 - Flags: review+
Keywords: checkin-needed
https://hg.mozilla.org/mozilla-central/rev/fca18fabdbbd
https://hg.mozilla.org/mozilla-central/rev/3c487660d38f
Status: ASSIGNED → RESOLVED
Closed: 5 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla33
Keywords: dev-doc-needed
Whiteboard: [DocArea=JS]
relnote-firefox: --- → ?
(Set flag on meta bug instead, sorry for bugspam.)
relnote-firefox: ? → ---
You need to log in before you can comment on or make changes to this bug.