Support await expressions
Adds support for `await` expressions. We have primarily seen await used inside callbacks, not directly within component render logic, but because we construct HIR for lambdas it is helpful to be able to model await rather than require everyone to rewrite to use the Promise API. Note a subtlety: awaiting a promise is a mutative operation, so we a) model it as a Mutate effect and b) avoid DCE of await expressions since they may cause side effects. See the test cases for examples.
Joe Savona committed
Mar 27, 2023 at 10:41 UTC
445e550e0044790d5bf971051412f1bb6defd5a3
13 files changed
+124
compiler/forget/src/HIR/BuildHIR.ts
+8
@@ -1687,6 +1687,14 @@ function lowerExpression(
1687
};
1688
}
1689
}
1690
+ case "AwaitExpression": {
1691
+ let expr = exprPath as NodePath<t.AwaitExpression>;
1692
+ return {
1693
+ kind: "Await",
1694
+ value: lowerExpressionToTemporary(builder, expr.get("argument")),
1695
+ loc: exprLoc,
1696
+ };
1697
+ }
1698
case "TypeCastExpression": {
1699
let expr = exprPath as NodePath<t.TypeCastExpression>;
1700
return {
compiler/forget/src/HIR/HIR.ts
+5
@@ -632,6 +632,11 @@ export type InstructionValue =
632
quasis: Array<{ raw: string; cooked?: string }>;
633
loc: SourceLocation;
634
}
635
+ | {
636
+ kind: "Await";
637
+ value: Place;
638
+ loc: SourceLocation;
639
+ }
640
/**
641
* Catch-all for statements such as type imports, nested class declarations, etc
642
* which are not directly represented, but included for completeness and to allow
compiler/forget/src/HIR/PrintHIR.ts
+4
@@ -448,6 +448,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
448
value = `RegExp /${instrValue.pattern}/${instrValue.flags}`;
449
break;
450
}
451
+ case "Await": {
452
+ value = `Await ${printPlace(instrValue.value)}`;
453
+ break;
454
+ }
455
default: {
456
assertExhaustive(
457
instrValue,
compiler/forget/src/HIR/visitors.ts
+8
@@ -170,6 +170,10 @@ export function* eachInstructionValueOperand(
170
yield* instrValue.subexprs;
171
break;
172
}
173
+ case "Await": {
174
+ yield instrValue.value;
175
+ break;
176
+ }
177
case "RegExpLiteral":
178
case "LoadGlobal":
179
case "UnsupportedNode":
@@ -423,6 +427,10 @@ export function mapInstructionOperands(
427
instrValue.subexprs = instrValue.subexprs.map(fn);
428
break;
429
}
430
+ case "Await": {
431
+ instrValue.value = fn(instrValue.value);
432
+ break;
433
+ }
434
case "RegExpLiteral":
435
case "LoadGlobal":
436
case "UnsupportedNode":
compiler/forget/src/Inference/InferReferenceEffects.ts
+11
@@ -788,6 +788,17 @@ function inferBlock(
788
state.define(lvalue, instrValue);
789
continue;
790
}
791
+ case "Await": {
792
+ state.initialize(instrValue, state.kind(instrValue.value));
793
+ // Awaiting a value causes it to change state (go from unresolved to resolved or error)
794
+ // It also means that any side-effects which would occur as part of the promise evaluation
795
+ // will occur.
796
+ state.reference(instrValue.value, Effect.Mutate);
797
+ const lvalue = instr.lvalue;
798
+ lvalue.effect = Effect.Mutate;
799
+ state.alias(lvalue, instrValue.value);
800
+ continue;
801
+ }
802
case "TypeCastExpression": {
803
// A type cast expression has no effect at runtime, so it's equivalent to a raw
804
// identifier:
compiler/forget/src/Optimization/DeadCodeElimination.ts
+1
@@ -190,6 +190,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
190
}
191
return true;
192
}
193
+ case "Await":
194
case "CallExpression":
195
case "ComputedDelete":
196
case "ComputedStore":
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+4
@@ -907,6 +907,10 @@ function codegenInstructionValue(
907
value = t.regExpLiteral(instrValue.pattern, instrValue.flags);
908
break;
909
}
910
+ case "Await": {
911
+ value = t.awaitExpression(codegenPlace(cx, instrValue.value));
912
+ break;
913
+ }
914
case "DeclareLocal":
915
case "Destructure":
916
case "StoreLocal": {
compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts
+1
@@ -219,6 +219,7 @@ function mayAllocate(value: InstructionValue): boolean {
219
case "Destructure": {
220
return doesPatternContainSpreadElement(value.lvalue.pattern);
221
}
222
+ case "Await":
223
case "DeclareLocal":
224
case "StoreLocal":
225
case "LoadGlobal":
compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts
+1
@@ -424,6 +424,7 @@ function computeMemoizationInputs(
424
rvalues: [],
425
};
426
}
427
+ case "Await":
428
case "TypeCastExpression": {
429
return {
430
// Indirection for the inner value, memoized if the value is
compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.expect.md
new
+32
@@ -0,0 +1,32 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+async function Component(props) {
6
+ const x = [];
7
+ await populateData(props.id, x);
8
+ return x;
9
+}
10
+
11
+```
12
+
13
+## Code
14
+
15
+```javascript
16
+async function Component(props) {
17
+ const $ = React.unstable_useMemoCache(2);
18
+ const c_0 = $[0] !== props.id;
19
+ let x;
20
+ if (c_0) {
21
+ x = [];
22
+ await populateData(props.id, x);
23
+ $[0] = props.id;
24
+ $[1] = x;
25
+ } else {
26
+ x = $[1];
27
+ }
28
+ return x;
29
+}
30
+
31
+```
32
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.js
new
+5
@@ -0,0 +1,5 @@
1
+async function Component(props) {
2
+ const x = [];
3
+ await populateData(props.id, x);
4
+ return x;
5
+}
compiler/forget/src/__tests__/fixtures/compiler/await.expect.md
new
+40
@@ -0,0 +1,40 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+async function Component(props) {
6
+ const user = await load(props.id);
7
+ return <div>{user.name}</div>;
8
+}
9
+
10
+```
11
+
12
+## Code
13
+
14
+```javascript
15
+async function Component(props) {
16
+ const $ = React.unstable_useMemoCache(4);
17
+ const c_0 = $[0] !== props.id;
18
+ let t0;
19
+ if (c_0) {
20
+ t0 = await load(props.id);
21
+ $[0] = props.id;
22
+ $[1] = t0;
23
+ } else {
24
+ t0 = $[1];
25
+ }
26
+ const user = t0;
27
+ const c_2 = $[2] !== user.name;
28
+ let t1;
29
+ if (c_2) {
30
+ t1 = <div>{user.name}</div>;
31
+ $[2] = user.name;
32
+ $[3] = t1;
33
+ } else {
34
+ t1 = $[3];
35
+ }
36
+ return t1;
37
+}
38
+
39
+```
40
+
\ No newline at end of file
compiler/forget/src/__tests__/fixtures/compiler/await.js
new
+4
@@ -0,0 +1,4 @@
1
+async function Component(props) {
2
+ const user = await load(props.id);
3
+ return <div>{user.name}</div>;
4
+}