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.
This commit is contained in:
Joe Savona
2023-03-27 10:41:49 -07:00
parent 4a826985db
commit 445e550e00
13 changed files with 124 additions and 0 deletions
+8
View File
@@ -1687,6 +1687,14 @@ function lowerExpression(
};
}
}
case "AwaitExpression": {
let expr = exprPath as NodePath<t.AwaitExpression>;
return {
kind: "Await",
value: lowerExpressionToTemporary(builder, expr.get("argument")),
loc: exprLoc,
};
}
case "TypeCastExpression": {
let expr = exprPath as NodePath<t.TypeCastExpression>;
return {
+5
View File
@@ -632,6 +632,11 @@ export type InstructionValue =
quasis: Array<{ raw: string; cooked?: string }>;
loc: SourceLocation;
}
| {
kind: "Await";
value: Place;
loc: SourceLocation;
}
/**
* Catch-all for statements such as type imports, nested class declarations, etc
* which are not directly represented, but included for completeness and to allow
+4
View File
@@ -448,6 +448,10 @@ export function printInstructionValue(instrValue: ReactiveValue): string {
value = `RegExp /${instrValue.pattern}/${instrValue.flags}`;
break;
}
case "Await": {
value = `Await ${printPlace(instrValue.value)}`;
break;
}
default: {
assertExhaustive(
instrValue,
+8
View File
@@ -170,6 +170,10 @@ export function* eachInstructionValueOperand(
yield* instrValue.subexprs;
break;
}
case "Await": {
yield instrValue.value;
break;
}
case "RegExpLiteral":
case "LoadGlobal":
case "UnsupportedNode":
@@ -423,6 +427,10 @@ export function mapInstructionOperands(
instrValue.subexprs = instrValue.subexprs.map(fn);
break;
}
case "Await": {
instrValue.value = fn(instrValue.value);
break;
}
case "RegExpLiteral":
case "LoadGlobal":
case "UnsupportedNode":
@@ -788,6 +788,17 @@ function inferBlock(
state.define(lvalue, instrValue);
continue;
}
case "Await": {
state.initialize(instrValue, state.kind(instrValue.value));
// Awaiting a value causes it to change state (go from unresolved to resolved or error)
// It also means that any side-effects which would occur as part of the promise evaluation
// will occur.
state.reference(instrValue.value, Effect.Mutate);
const lvalue = instr.lvalue;
lvalue.effect = Effect.Mutate;
state.alias(lvalue, instrValue.value);
continue;
}
case "TypeCastExpression": {
// A type cast expression has no effect at runtime, so it's equivalent to a raw
// identifier:
@@ -190,6 +190,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean {
}
return true;
}
case "Await":
case "CallExpression":
case "ComputedDelete":
case "ComputedStore":
@@ -907,6 +907,10 @@ function codegenInstructionValue(
value = t.regExpLiteral(instrValue.pattern, instrValue.flags);
break;
}
case "Await": {
value = t.awaitExpression(codegenPlace(cx, instrValue.value));
break;
}
case "DeclareLocal":
case "Destructure":
case "StoreLocal": {
@@ -219,6 +219,7 @@ function mayAllocate(value: InstructionValue): boolean {
case "Destructure": {
return doesPatternContainSpreadElement(value.lvalue.pattern);
}
case "Await":
case "DeclareLocal":
case "StoreLocal":
case "LoadGlobal":
@@ -424,6 +424,7 @@ function computeMemoizationInputs(
rvalues: [],
};
}
case "Await":
case "TypeCastExpression": {
return {
// Indirection for the inner value, memoized if the value is
@@ -0,0 +1,32 @@
## Input
```javascript
async function Component(props) {
const x = [];
await populateData(props.id, x);
return x;
}
```
## Code
```javascript
async function Component(props) {
const $ = React.unstable_useMemoCache(2);
const c_0 = $[0] !== props.id;
let x;
if (c_0) {
x = [];
await populateData(props.id, x);
$[0] = props.id;
$[1] = x;
} else {
x = $[1];
}
return x;
}
```
@@ -0,0 +1,5 @@
async function Component(props) {
const x = [];
await populateData(props.id, x);
return x;
}
@@ -0,0 +1,40 @@
## Input
```javascript
async function Component(props) {
const user = await load(props.id);
return <div>{user.name}</div>;
}
```
## Code
```javascript
async function Component(props) {
const $ = React.unstable_useMemoCache(4);
const c_0 = $[0] !== props.id;
let t0;
if (c_0) {
t0 = await load(props.id);
$[0] = props.id;
$[1] = t0;
} else {
t0 = $[1];
}
const user = t0;
const c_2 = $[2] !== user.name;
let t1;
if (c_2) {
t1 = <div>{user.name}</div>;
$[2] = user.name;
$[3] = t1;
} else {
t1 = $[3];
}
return t1;
}
```
@@ -0,0 +1,4 @@
async function Component(props) {
const user = await load(props.id);
return <div>{user.name}</div>;
}