@samitouri / QOS-React-2 / commits / 445e550e00

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 +}