Skip to content

Commit 98ffee6

Browse files
Perryvwtomblind
andauthored
Async await (#1093)
* Initial lualib promise class implementation * First promise tests * More promise tests * Promise class implementation * Implemented Promise.all * Promise.any * Promise.race * Promise.allSettled * fix prettier * Add promise example usage test * Added missing lualib dependencies for PromiseConstructor functions * Immediately call then/catch/finally callbacks on promises that are already resolved * Transform all references to Promise to __TS__Promise * PR feedback * Removed incorrect asyncs * Add test for direct chaining * Add test for finally and correct wrong behavior it caught * Added test throwing in parallel and chained then onFulfilleds * Fixed pull request link in ArrayIsArray lualib comment * Initial async await * Disallow await in top-level scope * Add await rejection test * Give await the correct lualib dependencies * use coroutine.status instead of lastData * Better top level await check * Add tests for async lambdas and throws in async functions * Moved toplevel await check to transformAwaitExpression and removed superfluous try/catch * fix for vararg access in async functions (#1096) Co-authored-by: Tom <tomblind@users.noreply.github.com> Co-authored-by: Tom <26638278+tomblind@users.noreply.github.com> Co-authored-by: Tom <tomblind@users.noreply.github.com>
1 parent b0ef3b0 commit 98ffee6

File tree

9 files changed

+479
-1
lines changed

9 files changed

+479
-1
lines changed

src/LuaLib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export enum LuaLibFeature {
2828
ArrayFlat = "ArrayFlat",
2929
ArrayFlatMap = "ArrayFlatMap",
3030
ArraySetLength = "ArraySetLength",
31+
Await = "Await",
3132
Class = "Class",
3233
ClassExtends = "ClassExtends",
3334
CloneDescriptor = "CloneDescriptor",
@@ -101,6 +102,7 @@ const luaLibDependencies: Partial<Record<LuaLibFeature, LuaLibFeature[]>> = {
101102
ArrayConcat: [LuaLibFeature.ArrayIsArray],
102103
ArrayFlat: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray],
103104
ArrayFlatMap: [LuaLibFeature.ArrayConcat, LuaLibFeature.ArrayIsArray],
105+
Await: [LuaLibFeature.InstanceOf, LuaLibFeature.New],
104106
Decorate: [LuaLibFeature.ObjectGetOwnPropertyDescriptor, LuaLibFeature.SetDescriptor, LuaLibFeature.ObjectAssign],
105107
DelegatedYield: [LuaLibFeature.StringAccess],
106108
Delete: [LuaLibFeature.ObjectGetOwnPropertyDescriptors],

src/lualib/Await.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// The following is a translation of the TypeScript async awaiter which uses generators and yields.
2+
// For Lua we use coroutines instead.
3+
//
4+
// Source:
5+
//
6+
// var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
7+
// function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
8+
// return new (P || (P = Promise))(function (resolve, reject) {
9+
// function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
10+
// function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
11+
// function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
12+
// step((generator = generator.apply(thisArg, _arguments || [])).next());
13+
// });
14+
// };
15+
//
16+
17+
// eslint-disable-next-line @typescript-eslint/promise-function-async
18+
function __TS__AsyncAwaiter(this: void, generator: (this: void) => void) {
19+
return new Promise((resolve, reject) => {
20+
const asyncCoroutine = coroutine.create(generator);
21+
22+
// eslint-disable-next-line @typescript-eslint/promise-function-async
23+
function adopt(value: unknown) {
24+
return value instanceof __TS__Promise ? value : Promise.resolve(value);
25+
}
26+
function fulfilled(value) {
27+
const [success, resultOrError] = coroutine.resume(asyncCoroutine, value);
28+
if (success) {
29+
step(resultOrError);
30+
} else {
31+
reject(resultOrError);
32+
}
33+
}
34+
function step(result: unknown) {
35+
if (coroutine.status(asyncCoroutine) === "dead") {
36+
resolve(result);
37+
} else {
38+
adopt(result).then(fulfilled, reason => reject(reason));
39+
}
40+
}
41+
const [success, resultOrError] = coroutine.resume(asyncCoroutine);
42+
if (success) {
43+
step(resultOrError);
44+
} else {
45+
reject(resultOrError);
46+
}
47+
});
48+
}
49+
50+
function __TS__Await(this: void, thing: unknown) {
51+
return coroutine.yield(thing);
52+
}

src/transformation/utils/diagnostics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,7 @@ export const annotationDeprecated = createWarningDiagnosticFactory(
147147
export const notAllowedOptionalAssignment = createErrorDiagnosticFactory(
148148
"The left-hand side of an assignment expression may not be an optional property access."
149149
);
150+
151+
export const awaitMustBeInAsyncFunction = createErrorDiagnosticFactory(
152+
"Await can only be used inside async functions."
153+
);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as ts from "typescript";
2+
import * as lua from "../../LuaAST";
3+
import { FunctionVisitor, TransformationContext } from "../context";
4+
import { awaitMustBeInAsyncFunction } from "../utils/diagnostics";
5+
import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
6+
import { findFirstNodeAbove } from "../utils/typescript";
7+
8+
export const transformAwaitExpression: FunctionVisitor<ts.AwaitExpression> = (node, context) => {
9+
// Check if await is inside an async function, it is not allowed at top level or in non-async functions
10+
const containingFunction = findFirstNodeAbove(node, ts.isFunctionLike);
11+
if (
12+
containingFunction === undefined ||
13+
!containingFunction.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)
14+
) {
15+
context.diagnostics.push(awaitMustBeInAsyncFunction(node));
16+
}
17+
18+
const expression = context.transformExpression(node.expression);
19+
return transformLuaLibFunction(context, LuaLibFeature.Await, node, expression);
20+
};
21+
22+
export function isAsyncFunction(declaration: ts.FunctionLikeDeclaration): boolean {
23+
return declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
24+
}
25+
26+
export function wrapInAsyncAwaiter(context: TransformationContext, statements: lua.Statement[]): lua.Statement[] {
27+
importLuaLibFeature(context, LuaLibFeature.Await);
28+
29+
return [
30+
lua.createReturnStatement([
31+
lua.createCallExpression(lua.createIdentifier("__TS__AsyncAwaiter"), [
32+
lua.createFunctionExpression(lua.createBlock(statements)),
33+
]),
34+
]),
35+
];
36+
}

src/transformation/visitors/function.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "../utils/lua-ast";
1414
import { LuaLibFeature, transformLuaLibFunction } from "../utils/lualib";
1515
import { peekScope, performHoisting, popScope, pushScope, Scope, ScopeType } from "../utils/scope";
16+
import { isAsyncFunction, wrapInAsyncAwaiter } from "./async-await";
1617
import { transformIdentifier } from "./identifier";
1718
import { transformExpressionBodyToReturnStatement } from "./return";
1819
import { transformBindingPattern } from "./variable-declaration";
@@ -114,7 +115,10 @@ export function transformFunctionBody(
114115
): [lua.Statement[], Scope] {
115116
const scope = pushScope(context, ScopeType.Function);
116117
scope.node = node;
117-
const bodyStatements = transformFunctionBodyContent(context, body);
118+
let bodyStatements = transformFunctionBodyContent(context, body);
119+
if (node && isAsyncFunction(node)) {
120+
bodyStatements = wrapInAsyncAwaiter(context, bodyStatements);
121+
}
118122
const headerStatements = transformFunctionBodyHeader(context, scope, parameters, spreadIdentifier);
119123
popScope(context);
120124
return [[...headerStatements, ...bodyStatements], scope];
@@ -195,6 +199,7 @@ export function transformFunctionToExpression(
195199
spreadIdentifier,
196200
node
197201
);
202+
198203
const functionExpression = lua.createFunctionExpression(
199204
lua.createBlock(transformedBody),
200205
paramNames,

src/transformation/visitors/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { typescriptVisitors } from "./typescript";
4141
import { transformPostfixUnaryExpression, transformPrefixUnaryExpression } from "./unary-expression";
4242
import { transformVariableStatement } from "./variable-declaration";
4343
import { jsxVisitors } from "./jsx/jsx";
44+
import { transformAwaitExpression } from "./async-await";
4445

4546
const transformEmptyStatement: FunctionVisitor<ts.EmptyStatement> = () => undefined;
4647
const transformParenthesizedExpression: FunctionVisitor<ts.ParenthesizedExpression> = (node, context) =>
@@ -51,6 +52,7 @@ export const standardVisitors: Visitors = {
5152
...typescriptVisitors,
5253
...jsxVisitors,
5354
[ts.SyntaxKind.ArrowFunction]: transformFunctionLikeDeclaration,
55+
[ts.SyntaxKind.AwaitExpression]: transformAwaitExpression,
5456
[ts.SyntaxKind.BinaryExpression]: transformBinaryExpression,
5557
[ts.SyntaxKind.Block]: transformBlock,
5658
[ts.SyntaxKind.BreakStatement]: transformBreakStatement,

src/transformation/visitors/sourceFile.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const transformSourceFileNode: FunctionVisitor<ts.SourceFile> = (node, co
2323
}
2424
} else {
2525
pushScope(context, ScopeType.File);
26+
2627
statements = performHoisting(context, context.transformStatements(node.statements));
2728
popScope(context);
2829

src/transformation/visitors/spread.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ export function isOptimizedVarArgSpread(context: TransformationContext, symbol:
3737
return false;
3838
}
3939

40+
// Scope cannot be an async function
41+
if (scope.node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword)) {
42+
return false;
43+
}
44+
4045
// Identifier must be a vararg in the local function scope's parameters
4146
const isSpreadParameter = (p: ts.ParameterDeclaration) =>
4247
p.dotDotDotToken && ts.isIdentifier(p.name) && context.checker.getSymbolAtLocation(p.name) === symbol;

0 commit comments

Comments
 (0)