Skip to content

Commit e7169db

Browse files
committed
implement lowering of for await loops
1 parent af4d944 commit e7169db

11 files changed

+548
-28
lines changed

CHANGELOG.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,39 @@
22

33
## Unreleased
44

5+
* Lower `for await` loops
6+
7+
This release lowers `for await` loops to the equivalent `for` loop containing `await` when esbuild is configured such that `for await` loops are unsupported. This transform still requires at least generator functions to be supported since esbuild's lowering of `await` currently relies on generators. This new transformation is modeled after what the TypeScript compiler does. Here's an example:
8+
9+
```js
10+
async function f() {
11+
for await (let x of y)
12+
x()
13+
}
14+
```
15+
16+
The code above will now become the following code with `--target=es2017` (omitting the code for the `__forAwait` helper function):
17+
18+
```js
19+
async function f() {
20+
try {
21+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
22+
let x = temp.value;
23+
x();
24+
}
25+
} catch (temp) {
26+
error = [temp];
27+
} finally {
28+
try {
29+
more && (temp = iter.return) && await temp.call(iter);
30+
} finally {
31+
if (error)
32+
throw error[0];
33+
}
34+
}
35+
}
36+
```
37+
538
* Automatically fix invalid `supported` configurations ([#2497](https://github.com/evanw/esbuild/issues/2497))
639

740
The `--target=` setting lets you tell esbuild to target a specific version of one or more JavaScript runtimes such as `chrome80,node14` and esbuild will restrict its output to only those features supported by all targeted JavaScript runtimes. More recently, esbuild introduced the `--supported:` setting that lets you override which features are supported on a per-feature basis. However, this now lets you configure nonsensical things such as `--supported:async-await=false --supported:async-generator=true`. Previously doing this could result in esbuild building successfully but producing invalid output.
@@ -15,7 +48,6 @@
1548

1649
* `generator=false` implies:
1750
* `async-generator=false`
18-
* `for-await=false`
1951

2052
* `class-field=false` implies:
2153
* `class-private-field=false`

internal/bundler/bundler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2134,7 +2134,7 @@ func applyOptionDefaults(options *config.Options) {
21342134

21352135
// Automatically fix invalid configurations of unsupported features
21362136
fixInvalidUnsupportedJSFeatureOverrides(options, compat.AsyncAwait, compat.AsyncGenerator|compat.ForAwait|compat.TopLevelAwait)
2137-
fixInvalidUnsupportedJSFeatureOverrides(options, compat.Generator, compat.AsyncGenerator|compat.ForAwait)
2137+
fixInvalidUnsupportedJSFeatureOverrides(options, compat.Generator, compat.AsyncGenerator)
21382138
fixInvalidUnsupportedJSFeatureOverrides(options, compat.ClassField, compat.ClassPrivateField)
21392139
fixInvalidUnsupportedJSFeatureOverrides(options, compat.ClassStaticField, compat.ClassPrivateStaticField)
21402140
fixInvalidUnsupportedJSFeatureOverrides(options, compat.Class,

internal/bundler/bundler_lower_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,3 +2297,45 @@ func TestLowerRegExpNameCollision(t *testing.T) {
22972297
},
22982298
})
22992299
}
2300+
2301+
func TestLowerForAwait2017(t *testing.T) {
2302+
lower_suite.expectBundled(t, bundled{
2303+
files: map[string]string{
2304+
"/entry.js": `
2305+
export default [
2306+
async () => { for await (x of y) z(x) },
2307+
async () => { for await (x.y of y) z(x) },
2308+
async () => { for await (let x of y) z(x) },
2309+
async () => { for await (const x of y) z(x) },
2310+
]
2311+
`,
2312+
},
2313+
entryPaths: []string{"/entry.js"},
2314+
options: config.Options{
2315+
Mode: config.ModePassThrough,
2316+
AbsOutputFile: "/out.js",
2317+
UnsupportedJSFeatures: es(2017),
2318+
},
2319+
})
2320+
}
2321+
2322+
func TestLowerForAwait2015(t *testing.T) {
2323+
lower_suite.expectBundled(t, bundled{
2324+
files: map[string]string{
2325+
"/entry.js": `
2326+
export default [
2327+
async () => { for await (x of y) z(x) },
2328+
async () => { for await (x.y of y) z(x) },
2329+
async () => { for await (let x of y) z(x) },
2330+
async () => { for await (const x of y) z(x) },
2331+
]
2332+
`,
2333+
},
2334+
entryPaths: []string{"/entry.js"},
2335+
options: config.Options{
2336+
Mode: config.ModePassThrough,
2337+
AbsOutputFile: "/out.js",
2338+
UnsupportedJSFeatures: es(2015),
2339+
},
2340+
})
2341+
}

internal/bundler/snapshots/snapshots_lower.txt

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,154 @@ export { ns };
626626
let ns2 = 123;
627627
export { ns2 as sn };
628628

629+
================================================================================
630+
TestLowerForAwait2015
631+
---------- /out.js ----------
632+
export default [
633+
() => __async(void 0, null, function* () {
634+
try {
635+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) {
636+
x = temp.value;
637+
z(x);
638+
}
639+
} catch (temp) {
640+
error = [temp];
641+
} finally {
642+
try {
643+
more && (temp = iter.return) && (yield temp.call(iter));
644+
} finally {
645+
if (error)
646+
throw error[0];
647+
}
648+
}
649+
}),
650+
() => __async(void 0, null, function* () {
651+
try {
652+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) {
653+
x.y = temp.value;
654+
z(x);
655+
}
656+
} catch (temp) {
657+
error = [temp];
658+
} finally {
659+
try {
660+
more && (temp = iter.return) && (yield temp.call(iter));
661+
} finally {
662+
if (error)
663+
throw error[0];
664+
}
665+
}
666+
}),
667+
() => __async(void 0, null, function* () {
668+
try {
669+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) {
670+
let x2 = temp.value;
671+
z(x2);
672+
}
673+
} catch (temp) {
674+
error = [temp];
675+
} finally {
676+
try {
677+
more && (temp = iter.return) && (yield temp.call(iter));
678+
} finally {
679+
if (error)
680+
throw error[0];
681+
}
682+
}
683+
}),
684+
() => __async(void 0, null, function* () {
685+
try {
686+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = yield iter.next()).done; more = false) {
687+
const x2 = temp.value;
688+
z(x2);
689+
}
690+
} catch (temp) {
691+
error = [temp];
692+
} finally {
693+
try {
694+
more && (temp = iter.return) && (yield temp.call(iter));
695+
} finally {
696+
if (error)
697+
throw error[0];
698+
}
699+
}
700+
})
701+
];
702+
703+
================================================================================
704+
TestLowerForAwait2017
705+
---------- /out.js ----------
706+
export default [
707+
async () => {
708+
try {
709+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
710+
x = temp.value;
711+
z(x);
712+
}
713+
} catch (temp) {
714+
error = [temp];
715+
} finally {
716+
try {
717+
more && (temp = iter.return) && await temp.call(iter);
718+
} finally {
719+
if (error)
720+
throw error[0];
721+
}
722+
}
723+
},
724+
async () => {
725+
try {
726+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
727+
x.y = temp.value;
728+
z(x);
729+
}
730+
} catch (temp) {
731+
error = [temp];
732+
} finally {
733+
try {
734+
more && (temp = iter.return) && await temp.call(iter);
735+
} finally {
736+
if (error)
737+
throw error[0];
738+
}
739+
}
740+
},
741+
async () => {
742+
try {
743+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
744+
let x2 = temp.value;
745+
z(x2);
746+
}
747+
} catch (temp) {
748+
error = [temp];
749+
} finally {
750+
try {
751+
more && (temp = iter.return) && await temp.call(iter);
752+
} finally {
753+
if (error)
754+
throw error[0];
755+
}
756+
}
757+
},
758+
async () => {
759+
try {
760+
for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) {
761+
const x2 = temp.value;
762+
z(x2);
763+
}
764+
} catch (temp) {
765+
error = [temp];
766+
} finally {
767+
try {
768+
more && (temp = iter.return) && await temp.call(iter);
769+
} finally {
770+
if (error)
771+
throw error[0];
772+
}
773+
}
774+
}
775+
];
776+
629777
================================================================================
630778
TestLowerNullishCoalescingAssignmentIssue1493
631779
---------- /out.js ----------

internal/js_parser/js_parser.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6595,10 +6595,16 @@ func (p *parser) parseStmt(opts parseStmtOpts) js_ast.Stmt {
65956595
p.log.AddError(&p.tracker, awaitRange, "Cannot use \"await\" outside an async function")
65966596
isForAwait = false
65976597
} else {
6598-
didGenerateError := p.markSyntaxFeature(compat.ForAwait, awaitRange)
6599-
if p.fnOrArrowDataParse.isTopLevel && !didGenerateError {
6598+
didGenerateError := false
6599+
if p.fnOrArrowDataParse.isTopLevel {
66006600
p.topLevelAwaitKeyword = awaitRange
6601-
p.markSyntaxFeature(compat.TopLevelAwait, awaitRange)
6601+
didGenerateError = p.markSyntaxFeature(compat.TopLevelAwait, awaitRange)
6602+
}
6603+
if !didGenerateError && p.options.unsupportedJSFeatures.Has(compat.AsyncAwait) && p.options.unsupportedJSFeatures.Has(compat.Generator) {
6604+
// If for-await loops aren't supported, then we only support lowering
6605+
// if either async/await or generators is supported. Otherwise we
6606+
// cannot lower for-await loops.
6607+
p.markSyntaxFeature(compat.ForAwait, awaitRange)
66026608
}
66036609
}
66046610
p.lexer.Next()
@@ -9827,6 +9833,10 @@ func (p *parser) visitAndAppendStmt(stmts []js_ast.Stmt, stmt js_ast.Stmt) []js_
98279833

98289834
p.lowerObjectRestInForLoopInit(s.Init, &s.Body)
98299835

9836+
if s.IsAwait && p.options.unsupportedJSFeatures.Has(compat.ForAwait) {
9837+
return p.lowerForAwaitLoop(stmt.Loc, s, stmts)
9838+
}
9839+
98309840
case *js_ast.STry:
98319841
p.pushScopeForVisitPass(js_ast.ScopeBlock, stmt.Loc)
98329842
if p.fnOrArrowDataVisit.tryBodyCount == 0 {

0 commit comments

Comments
 (0)