From 445e550e0044790d5bf971051412f1bb6defd5a3 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Mon, 27 Mar 2023 10:41:49 -0700 Subject: [PATCH] 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. --- compiler/forget/src/HIR/BuildHIR.ts | 8 ++++ compiler/forget/src/HIR/HIR.ts | 5 +++ compiler/forget/src/HIR/PrintHIR.ts | 4 ++ compiler/forget/src/HIR/visitors.ts | 8 ++++ .../src/Inference/InferReferenceEffects.ts | 11 +++++ .../src/Optimization/DeadCodeElimination.ts | 1 + .../ReactiveScopes/CodegenReactiveFunction.ts | 4 ++ .../InferReactiveScopeVariables.ts | 1 + .../ReactiveScopes/PruneNonEscapingScopes.ts | 1 + .../await-side-effecting-promise.expect.md | 32 +++++++++++++++ .../compiler/await-side-effecting-promise.js | 5 +++ .../fixtures/compiler/await.expect.md | 40 +++++++++++++++++++ .../src/__tests__/fixtures/compiler/await.js | 4 ++ 13 files changed, 124 insertions(+) create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.js create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/await.expect.md create mode 100644 compiler/forget/src/__tests__/fixtures/compiler/await.js diff --git a/compiler/forget/src/HIR/BuildHIR.ts b/compiler/forget/src/HIR/BuildHIR.ts index c4724ac036..719c380a9d 100644 --- a/compiler/forget/src/HIR/BuildHIR.ts +++ b/compiler/forget/src/HIR/BuildHIR.ts @@ -1687,6 +1687,14 @@ function lowerExpression( }; } } + case "AwaitExpression": { + let expr = exprPath as NodePath; + return { + kind: "Await", + value: lowerExpressionToTemporary(builder, expr.get("argument")), + loc: exprLoc, + }; + } case "TypeCastExpression": { let expr = exprPath as NodePath; return { diff --git a/compiler/forget/src/HIR/HIR.ts b/compiler/forget/src/HIR/HIR.ts index 678c39471c..ef873fd87a 100644 --- a/compiler/forget/src/HIR/HIR.ts +++ b/compiler/forget/src/HIR/HIR.ts @@ -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 diff --git a/compiler/forget/src/HIR/PrintHIR.ts b/compiler/forget/src/HIR/PrintHIR.ts index f4d8e76fbe..8b82b77a59 100644 --- a/compiler/forget/src/HIR/PrintHIR.ts +++ b/compiler/forget/src/HIR/PrintHIR.ts @@ -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, diff --git a/compiler/forget/src/HIR/visitors.ts b/compiler/forget/src/HIR/visitors.ts index bca0080d68..328a995466 100644 --- a/compiler/forget/src/HIR/visitors.ts +++ b/compiler/forget/src/HIR/visitors.ts @@ -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": diff --git a/compiler/forget/src/Inference/InferReferenceEffects.ts b/compiler/forget/src/Inference/InferReferenceEffects.ts index fb04c74f79..c3cf9c7281 100644 --- a/compiler/forget/src/Inference/InferReferenceEffects.ts +++ b/compiler/forget/src/Inference/InferReferenceEffects.ts @@ -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: diff --git a/compiler/forget/src/Optimization/DeadCodeElimination.ts b/compiler/forget/src/Optimization/DeadCodeElimination.ts index fe7edaa888..877a2340a4 100644 --- a/compiler/forget/src/Optimization/DeadCodeElimination.ts +++ b/compiler/forget/src/Optimization/DeadCodeElimination.ts @@ -190,6 +190,7 @@ function pruneableValue(value: InstructionValue, state: State): boolean { } return true; } + case "Await": case "CallExpression": case "ComputedDelete": case "ComputedStore": diff --git a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts index 27c2263af1..05697c3a1b 100644 --- a/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -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": { diff --git a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts index 888cb83499..8f519f6e04 100644 --- a/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/forget/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -219,6 +219,7 @@ function mayAllocate(value: InstructionValue): boolean { case "Destructure": { return doesPatternContainSpreadElement(value.lvalue.pattern); } + case "Await": case "DeclareLocal": case "StoreLocal": case "LoadGlobal": diff --git a/compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts b/compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts index 51d81fe8bb..ec2ce6dbce 100644 --- a/compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts +++ b/compiler/forget/src/ReactiveScopes/PruneNonEscapingScopes.ts @@ -424,6 +424,7 @@ function computeMemoizationInputs( rvalues: [], }; } + case "Await": case "TypeCastExpression": { return { // Indirection for the inner value, memoized if the value is diff --git a/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.expect.md new file mode 100644 index 0000000000..aabe9a208a --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.expect.md @@ -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; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.js b/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.js new file mode 100644 index 0000000000..8b5a45e608 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/await-side-effecting-promise.js @@ -0,0 +1,5 @@ +async function Component(props) { + const x = []; + await populateData(props.id, x); + return x; +} diff --git a/compiler/forget/src/__tests__/fixtures/compiler/await.expect.md b/compiler/forget/src/__tests__/fixtures/compiler/await.expect.md new file mode 100644 index 0000000000..f1b76412e7 --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/await.expect.md @@ -0,0 +1,40 @@ + +## Input + +```javascript +async function Component(props) { + const user = await load(props.id); + return
{user.name}
; +} + +``` + +## 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 =
{user.name}
; + $[2] = user.name; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + \ No newline at end of file diff --git a/compiler/forget/src/__tests__/fixtures/compiler/await.js b/compiler/forget/src/__tests__/fixtures/compiler/await.js new file mode 100644 index 0000000000..3d021138fe --- /dev/null +++ b/compiler/forget/src/__tests__/fixtures/compiler/await.js @@ -0,0 +1,4 @@ +async function Component(props) { + const user = await load(props.id); + return
{user.name}
; +}