Skip to content

Commit

Permalink
fix #1327: improve lowered template literals
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jun 3, 2021
1 parent 65127d2 commit 92d5449
Show file tree
Hide file tree
Showing 9 changed files with 290 additions and 57 deletions.
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,53 @@

## Unreleased

* Improve template literal lowering transformation conformance ([#1327](https://github.com/evanw/esbuild/issues/1327))

This release contains the following improvements to template literal lowering for environments that don't support tagged template literals natively (such as `--target=es5`):

* For tagged template literals, the arrays of strings that are passed to the tag function are now frozen and immutable. They are also now cached so they should now compare identical between multiple template evaluations:

```js
// Original code
console.log(tag`\u{10000}`)

// Old output
console.log(tag(__template(["𐀀"], ["\\u{10000}"])));

// New output
var _a;
console.log(tag(_a || (_a = __template(["𐀀"], ["\\u{10000}"]))));
```

* For tagged template literals, the generated code size is now smaller in the common case where there are no escape sequences, since in that case there is no distinction between "raw" and "cooked" values:

```js
// Original code
console.log(tag`some text without escape sequences`)

// Old output
console.log(tag(__template(["some text without escape sequences"], ["some text without escape sequences"])));

// New output
var _a;
console.log(tag(_a || (_a = __template(["some text without escape sequences"]))));
```

* For non-tagged template literals, the generated code now uses chains of `.concat()` calls instead of string addition:

```js
// Original code
console.log(`an ${example} template ${literal}`)

// Old output
console.log("an " + example + " template " + literal);

// New output
console.log("an ".concat(example, " template ").concat(literal));
```

The old output was incorrect for several reasons including that `toString` must be called instead of `valueOf` for objects and that passing a `Symbol` instance should throw instead of converting the symbol to a string. Using `.concat()` instead of string addition fixes both of those correctness issues. And you can't use a single `.concat()` call because side effects must happen inline instead of at the end.

* Only respect `target` in `tsconfig.json` when esbuild's target is not configured ([#1332](https://github.com/evanw/esbuild/issues/1332))

In version 0.12.4, esbuild began respecting the `target` setting in `tsconfig.json`. However, sometimes `tsconfig.json` contains target values that should not be used. With this release, esbuild will now only use the `target` value in `tsconfig.json` as the language level when esbuild's `target` setting is not configured. If esbuild's `target` setting is configured then the `target` value in `tsconfig.json` is now ignored.
Expand Down
27 changes: 27 additions & 0 deletions internal/bundler/bundler_lower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1544,3 +1544,30 @@ func TestLowerPrivateClassBrandCheckSupported(t *testing.T) {
},
})
}

func TestLowerTemplateObject(t *testing.T) {
lower_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
x = () => [
tag` + "`x`" + `,
tag` + "`\\xFF`" + `,
tag` + "`\\x`" + `,
tag` + "`\\u`" + `,
]
y = () => [
tag` + "`x${y}z`" + `,
tag` + "`\\xFF${y}z`" + `,
tag` + "`x${y}\\z`" + `,
tag` + "`x${y}\\u`" + `,
]
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModePassThrough,
AbsOutputFile: "/out.js",
UnsupportedJSFeatures: compat.TemplateLiteral,
},
})
}
17 changes: 17 additions & 0 deletions internal/bundler/snapshots/snapshots_lower.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,23 @@ x = y;
for (var x in {})
;

================================================================================
TestLowerTemplateObject
---------- /out.js ----------
var _a, _b, _c, _d, _e, _f, _g, _h;
x = () => [
tag(_a || (_a = __template(["x"]))),
tag(_b || (_b = __template(["ÿ"], ["\\xFF"]))),
tag(_c || (_c = __template([void 0], ["\\x"]))),
tag(_d || (_d = __template([void 0], ["\\u"])))
];
y = () => [
tag(_e || (_e = __template(["x", "z"])), y),
tag(_f || (_f = __template(["ÿ", "z"], ["\\xFF", "z"])), y),
tag(_g || (_g = __template(["x", "z"], ["x", "\\z"])), y),
tag(_h || (_h = __template(["x", void 0], ["x", "\\u"])), y)
];

================================================================================
TestTSLowerClassField2020NoBundle
---------- /out.js ----------
Expand Down
23 changes: 20 additions & 3 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,10 @@ type parser struct {
thenCatchChain thenCatchChain

// Temporary variables used for lowering
tempRefsToDeclare []tempRef
tempRefCount int
tempRefsToDeclare []tempRef
tempRefCount int
topLevelTempRefsToDeclare []tempRef
topLevelTempRefCount int

// When bundling, hoisted top-level local variables declared with "var" in
// nested scopes are moved up to be declared in the top-level scope instead.
Expand Down Expand Up @@ -6723,6 +6725,14 @@ func (p *parser) generateTempRef(declare generateTempRefArg, optionalName string
return ref
}

func (p *parser) generateTopLevelTempRef() js_ast.Ref {
ref := p.newSymbol(js_ast.SymbolOther, "_"+js_ast.DefaultNameMinifier.NumberToMinifiedName(p.topLevelTempRefCount))
p.topLevelTempRefsToDeclare = append(p.topLevelTempRefsToDeclare, tempRef{ref: ref})
p.moduleScope.Generated = append(p.moduleScope.Generated, ref)
p.topLevelTempRefCount++
return ref
}

func (p *parser) pushScopeForVisitPass(kind js_ast.ScopeKind, loc logger.Loc) {
order := p.scopesInOrder[0]

Expand Down Expand Up @@ -6939,6 +6949,12 @@ func (p *parser) visitStmtsAndPrependTempRefs(stmts []js_ast.Stmt, opts prependT
}
}

// There may also be special top-level-only temporaries to declare
if p.currentScope == p.moduleScope && p.topLevelTempRefsToDeclare != nil {
p.tempRefsToDeclare = append(p.tempRefsToDeclare, p.topLevelTempRefsToDeclare...)
p.topLevelTempRefsToDeclare = nil
}

// Prepend the generated temporary variables to the beginning of the statement list
if len(p.tempRefsToDeclare) > 0 {
decls := []js_ast.Decl{}
Expand Down Expand Up @@ -13037,7 +13053,8 @@ func (p *parser) appendPart(parts []js_ast.Part, stmts []js_ast.Stmt) []js_ast.P
p.importRecordsForCurrentPart = nil
p.scopesForCurrentPart = nil
part := js_ast.Part{
Stmts: p.visitStmtsAndPrependTempRefs(stmts, prependTempRefsOpts{}),
Stmts: p.visitStmtsAndPrependTempRefs(stmts, prependTempRefsOpts{}),

SymbolUses: p.symbolUses,
}

Expand Down
87 changes: 53 additions & 34 deletions internal/js_parser/js_parser_lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -2598,46 +2598,40 @@ func (p *parser) lowerTemplateLiteral(loc logger.Loc, e *js_ast.ETemplate) js_as
var value js_ast.Expr

// Handle the head
if len(e.HeadCooked) == 0 {
// "`${x}y`" => "x + 'y'"
part := e.Parts[0]
value = js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpAdd,
Left: part.Value,
Right: js_ast.Expr{Loc: part.TailLoc, Data: &js_ast.EString{
Value: part.TailCooked,
LegacyOctalLoc: e.LegacyOctalLoc,
}},
}}
e.Parts = e.Parts[1:]
} else {
// "`x${y}`" => "'x' + y"
value = js_ast.Expr{Loc: loc, Data: &js_ast.EString{
Value: e.HeadCooked,
LegacyOctalLoc: e.LegacyOctalLoc,
}}
}
value = js_ast.Expr{Loc: loc, Data: &js_ast.EString{
Value: e.HeadCooked,
LegacyOctalLoc: e.LegacyOctalLoc,
}}

// Handle the tail
// Handle the tail. Each one is handled with a separate call to ".concat()"
// to handle various corner cases in the specification including:
//
// * For objects, "toString" must be called instead of "valueOf"
// * Side effects must happen inline instead of at the end
// * Passing a "Symbol" instance should throw
//
for _, part := range e.Parts {
value = js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpAdd,
Left: value,
Right: part.Value,
}}
var args []js_ast.Expr
if len(part.TailCooked) > 0 {
value = js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpAdd,
Left: value,
Right: js_ast.Expr{Loc: part.TailLoc, Data: &js_ast.EString{Value: part.TailCooked}},
}}
args = []js_ast.Expr{part.Value, {Loc: part.TailLoc, Data: &js_ast.EString{Value: part.TailCooked}}}
} else {
args = []js_ast.Expr{part.Value}
}
value = js_ast.Expr{Loc: loc, Data: &js_ast.ECall{
Target: js_ast.Expr{Loc: loc, Data: &js_ast.EDot{
Target: value,
Name: "concat",
NameLoc: part.Value.Loc,
}},
Args: args,
}}
}

return value
}

// Otherwise, call the tag with the template object
needsRaw := false
cooked := []js_ast.Expr{}
raw := []js_ast.Expr{}
args := make([]js_ast.Expr, 0, 1+len(e.Parts))
Expand All @@ -2646,8 +2640,12 @@ func (p *parser) lowerTemplateLiteral(loc logger.Loc, e *js_ast.ETemplate) js_as
// Handle the head
if e.HeadCooked == nil {
cooked = append(cooked, js_ast.Expr{Loc: e.HeadLoc, Data: js_ast.EUndefinedShared})
needsRaw = true
} else {
cooked = append(cooked, js_ast.Expr{Loc: e.HeadLoc, Data: &js_ast.EString{Value: e.HeadCooked}})
if !js_lexer.UTF16EqualsString(e.HeadCooked, e.HeadRaw) {
needsRaw = true
}
}
raw = append(raw, js_ast.Expr{Loc: e.HeadLoc, Data: &js_ast.EString{Value: js_lexer.StringToUTF16(e.HeadRaw)}})

Expand All @@ -2656,18 +2654,39 @@ func (p *parser) lowerTemplateLiteral(loc logger.Loc, e *js_ast.ETemplate) js_as
args = append(args, part.Value)
if part.TailCooked == nil {
cooked = append(cooked, js_ast.Expr{Loc: part.TailLoc, Data: js_ast.EUndefinedShared})
needsRaw = true
} else {
cooked = append(cooked, js_ast.Expr{Loc: part.TailLoc, Data: &js_ast.EString{Value: part.TailCooked}})
if !js_lexer.UTF16EqualsString(part.TailCooked, part.TailRaw) {
needsRaw = true
}
}
raw = append(raw, js_ast.Expr{Loc: part.TailLoc, Data: &js_ast.EString{Value: js_lexer.StringToUTF16(part.TailRaw)}})
}

// Construct the template object
args[0] = p.callRuntime(e.HeadLoc, "__template", []js_ast.Expr{
{Loc: e.HeadLoc, Data: &js_ast.EArray{Items: cooked, IsSingleLine: true}},
{Loc: e.HeadLoc, Data: &js_ast.EArray{Items: raw, IsSingleLine: true}},
})
cookedArray := js_ast.Expr{Loc: e.HeadLoc, Data: &js_ast.EArray{Items: cooked, IsSingleLine: true}}
var arrays []js_ast.Expr
if needsRaw {
arrays = []js_ast.Expr{cookedArray, {Loc: e.HeadLoc, Data: &js_ast.EArray{Items: raw, IsSingleLine: true}}}
} else {
arrays = []js_ast.Expr{cookedArray}
}
templateObj := p.callRuntime(e.HeadLoc, "__template", arrays)

// Cache it in a temporary object (required by the specification)
tempRef := p.generateTopLevelTempRef()
args[0] = js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpLogicalOr,
Left: js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}},
Right: js_ast.Expr{Loc: loc, Data: &js_ast.EBinary{
Op: js_ast.BinOpAssign,
Left: js_ast.Expr{Loc: loc, Data: &js_ast.EIdentifier{Ref: tempRef}},
Right: templateObj,
}},
}}

// Call the tag function
return js_ast.Expr{Loc: loc, Data: &js_ast.ECall{
Target: e.TagOrNil,
Args: args,
Expand Down
32 changes: 16 additions & 16 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4567,22 +4567,22 @@ func TestES5(t *testing.T) {
expectParseErrorTarget(t, 5, "([...[x]])",
"<stdin>: error: Transforming array spread to the configured target environment is not supported yet\n")
expectPrintedTarget(t, 5, "`abc`;", "\"abc\";\n")
expectPrintedTarget(t, 5, "`a${b}`;", "\"a\" + b;\n")
expectPrintedTarget(t, 5, "`${a}b`;", "a + \"b\";\n")
expectPrintedTarget(t, 5, "`${a}${b}`;", "a + \"\" + b;\n")
expectPrintedTarget(t, 5, "`a${b}c`;", "\"a\" + b + \"c\";\n")
expectPrintedTarget(t, 5, "`a${b}${c}`;", "\"a\" + b + c;\n")
expectPrintedTarget(t, 5, "`a${b}${c}d`;", "\"a\" + b + c + \"d\";\n")
expectPrintedTarget(t, 5, "`a${b}c${d}`;", "\"a\" + b + \"c\" + d;\n")
expectPrintedTarget(t, 5, "`a${b}c${d}e`;", "\"a\" + b + \"c\" + d + \"e\";\n")
expectPrintedTarget(t, 5, "tag``;", "tag(__template([\"\"], [\"\"]));\n")
expectPrintedTarget(t, 5, "tag`abc`;", "tag(__template([\"abc\"], [\"abc\"]));\n")
expectPrintedTarget(t, 5, "tag`\\utf`;", "tag(__template([void 0], [\"\\\\utf\"]));\n")
expectPrintedTarget(t, 5, "tag`${a}b`;", "tag(__template([\"\", \"b\"], [\"\", \"b\"]), a);\n")
expectPrintedTarget(t, 5, "tag`a${b}`;", "tag(__template([\"a\", \"\"], [\"a\", \"\"]), b);\n")
expectPrintedTarget(t, 5, "tag`a${b}c`;", "tag(__template([\"a\", \"c\"], [\"a\", \"c\"]), b);\n")
expectPrintedTarget(t, 5, "tag`a${b}\\u`;", "tag(__template([\"a\", void 0], [\"a\", \"\\\\u\"]), b);\n")
expectPrintedTarget(t, 5, "tag`\\u${b}c`;", "tag(__template([void 0, \"c\"], [\"\\\\u\", \"c\"]), b);\n")
expectPrintedTarget(t, 5, "`a${b}`;", "\"a\".concat(b);\n")
expectPrintedTarget(t, 5, "`${a}b`;", "\"\".concat(a, \"b\");\n")
expectPrintedTarget(t, 5, "`${a}${b}`;", "\"\".concat(a).concat(b);\n")
expectPrintedTarget(t, 5, "`a${b}c`;", "\"a\".concat(b, \"c\");\n")
expectPrintedTarget(t, 5, "`a${b}${c}`;", "\"a\".concat(b).concat(c);\n")
expectPrintedTarget(t, 5, "`a${b}${c}d`;", "\"a\".concat(b).concat(c, \"d\");\n")
expectPrintedTarget(t, 5, "`a${b}c${d}`;", "\"a\".concat(b, \"c\").concat(d);\n")
expectPrintedTarget(t, 5, "`a${b}c${d}e`;", "\"a\".concat(b, \"c\").concat(d, \"e\");\n")
expectPrintedTarget(t, 5, "tag``;", "var _a;\ntag(_a || (_a = __template([\"\"])));\n")
expectPrintedTarget(t, 5, "tag`abc`;", "var _a;\ntag(_a || (_a = __template([\"abc\"])));\n")
expectPrintedTarget(t, 5, "tag`\\utf`;", "var _a;\ntag(_a || (_a = __template([void 0], [\"\\\\utf\"])));\n")
expectPrintedTarget(t, 5, "tag`${a}b`;", "var _a;\ntag(_a || (_a = __template([\"\", \"b\"])), a);\n")
expectPrintedTarget(t, 5, "tag`a${b}`;", "var _a;\ntag(_a || (_a = __template([\"a\", \"\"])), b);\n")
expectPrintedTarget(t, 5, "tag`a${b}c`;", "var _a;\ntag(_a || (_a = __template([\"a\", \"c\"])), b);\n")
expectPrintedTarget(t, 5, "tag`a${b}\\u`;", "var _a;\ntag(_a || (_a = __template([\"a\", void 0], [\"a\", \"\\\\u\"])), b);\n")
expectPrintedTarget(t, 5, "tag`\\u${b}c`;", "var _a;\ntag(_a || (_a = __template([void 0, \"c\"], [\"\\\\u\", \"c\"])), b);\n")
expectParseErrorTarget(t, 5, "class Foo { constructor() { new.target } }",
"<stdin>: error: Transforming class syntax to the configured target environment is not supported yet\n"+
"<stdin>: error: Transforming object literal extensions to the configured target environment is not supported yet\n"+
Expand Down
6 changes: 3 additions & 3 deletions internal/js_printer/js_printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -916,11 +916,11 @@ func TestAvoidSlashScript(t *testing.T) {
expectPrintedMinify(t, "x = 1 << /script/.exec(y).length", "x=1<< /script/.exec(y).length;")
expectPrinted(t, "//! </script", "//! <\\/script\n")
expectPrinted(t, "String.raw`</script`",
"String.raw(__template([\"<\\/script\"], [\"<\\/script\"]));\nimport {\n __template\n} from \"<runtime>\";\n")
"var _a;\nString.raw(_a || (_a = __template([\"<\\/script\"])));\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`</script${a}`",
"String.raw(__template([\"<\\/script\", \"\"], [\"<\\/script\", \"\"]), a);\nimport {\n __template\n} from \"<runtime>\";\n")
"var _a;\nString.raw(_a || (_a = __template([\"<\\/script\", \"\"])), a);\nimport {\n __template\n} from \"<runtime>\";\n")
expectPrinted(t, "String.raw`${a}</script`",
"String.raw(__template([\"\", \"<\\/script\"], [\"\", \"<\\/script\"]), a);\nimport {\n __template\n} from \"<runtime>\";\n")
"var _a;\nString.raw(_a || (_a = __template([\"\", \"<\\/script\"])), a);\nimport {\n __template\n} from \"<runtime>\";\n")

// Negative cases
expectPrinted(t, "x = '</'", "x = \"</\";\n")
Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ func code(isES6 bool) string {
}
// For lowering tagged template literals
export var __template = (cooked, raw) => __defProp(cooked, 'raw', { value: __freeze(raw) })
export var __template = (cooked, raw) => __freeze(__defProp(cooked, 'raw', { value: __freeze(raw || cooked.slice()) }))
// This helps for lowering async functions
export var __async = (__this, __arguments, generator) => {
Expand Down
Loading

0 comments on commit 92d5449

Please sign in to comment.