Skip to content

Commit 87dab53

Browse files
authored
Add support for TS 5.2 using and await using keywords (#1479)
* Support for using * Async await * Some renaming and comments
1 parent 1d23427 commit 87dab53

File tree

10 files changed

+379
-4
lines changed

10 files changed

+379
-4
lines changed

src/LuaLib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export enum LuaLibFeature {
110110
SymbolRegistry = "SymbolRegistry",
111111
TypeOf = "TypeOf",
112112
Unpack = "Unpack",
113+
Using = "Using",
114+
UsingAsync = "UsingAsync",
113115
}
114116

115117
export interface LuaLibFeatureInfo {

src/lualib/Symbol.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export function __TS__Symbol(this: void, description?: string | number): symbol
99
}
1010

1111
export const Symbol = {
12+
asyncDispose: __TS__Symbol("Symbol.asyncDispose"),
13+
dispose: __TS__Symbol("Symbol.dispose"),
1214
iterator: __TS__Symbol("Symbol.iterator"),
1315
hasInstance: __TS__Symbol("Symbol.hasInstance"),
1416

src/lualib/Using.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function __TS__Using<TArgs extends Disposable[], TReturn>(
2+
this: undefined,
3+
cb: (...args: TArgs) => TReturn,
4+
...args: TArgs
5+
): TReturn {
6+
let thrownError;
7+
const [ok, result] = xpcall(
8+
() => cb(...args),
9+
err => (thrownError = err)
10+
);
11+
12+
const argArray = [...args];
13+
for (let i = argArray.length - 1; i >= 0; i--) {
14+
argArray[i][Symbol.dispose]();
15+
}
16+
17+
if (!ok) {
18+
throw thrownError;
19+
}
20+
21+
return result;
22+
}

src/lualib/UsingAsync.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export async function __TS__UsingAsync<TArgs extends Array<Disposable | AsyncDisposable>, TReturn>(
2+
this: undefined,
3+
cb: (...args: TArgs) => TReturn,
4+
...args: TArgs
5+
): Promise<TReturn> {
6+
let thrownError;
7+
const [ok, result] = xpcall(
8+
() => cb(...args),
9+
err => (thrownError = err)
10+
);
11+
12+
const argArray = [...args];
13+
for (let i = argArray.length - 1; i >= 0; i--) {
14+
if (Symbol.dispose in argArray[i]) {
15+
(argArray[i] as Disposable)[Symbol.dispose]();
16+
}
17+
if (Symbol.asyncDispose in argArray[i]) {
18+
await (argArray[i] as AsyncDisposable)[Symbol.asyncDispose]();
19+
}
20+
}
21+
22+
if (!ok) {
23+
throw thrownError;
24+
}
25+
26+
return result;
27+
}

src/transformation/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as lua from "../LuaAST";
33
import { getOrUpdate } from "../utils";
44
import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context";
55
import { standardVisitors } from "./visitors";
6+
import { usingTransformer } from "./pre-transformers/using-transformer";
67

78
export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
89
const objectVisitorMap: Map<ts.SyntaxKind, Array<ObjectVisitor<ts.Node>>> = new Map();
@@ -32,7 +33,13 @@ export function createVisitorMap(customVisitors: Visitors[]): VisitorMap {
3233

3334
export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) {
3435
const context = new TransformationContext(program, sourceFile, visitorMap);
35-
const [file] = context.transformNode(sourceFile) as [lua.File];
36+
37+
// TS -> TS pre-transformation
38+
const preTransformers = [usingTransformer(context)];
39+
const result = ts.transform(sourceFile, preTransformers);
40+
41+
// TS -> Lua transformation
42+
const [file] = context.transformNode(result.transformed[0]) as [lua.File];
3643

3744
return { file, diagnostics: context.diagnostics };
3845
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as ts from "typescript";
2+
import { TransformationContext } from "../context";
3+
import { LuaLibFeature, importLuaLibFeature } from "../utils/lualib";
4+
5+
export function usingTransformer(context: TransformationContext): ts.TransformerFactory<ts.SourceFile> {
6+
return ctx => sourceFile => {
7+
function visit(node: ts.Node): ts.Node {
8+
if (ts.isBlock(node)) {
9+
const [hasUsings, newStatements] = transformBlockWithUsing(context, node.statements, node);
10+
if (hasUsings) {
11+
// Recurse visitor into updated block to find further usings
12+
const updatedBlock = ts.factory.updateBlock(node, newStatements);
13+
const result = ts.visitEachChild(updatedBlock, visit, ctx);
14+
15+
// Set all the synthetic node parents to something that makes sense
16+
const parent: ts.Node[] = [updatedBlock];
17+
function setParent(node2: ts.Node): ts.Node {
18+
ts.setParent(node2, parent[parent.length - 1]);
19+
parent.push(node2);
20+
ts.visitEachChild(node2, setParent, ctx);
21+
parent.push();
22+
return node2;
23+
}
24+
ts.visitEachChild(updatedBlock, setParent, ctx);
25+
ts.setParent(updatedBlock, node.parent);
26+
27+
return result;
28+
}
29+
}
30+
return ts.visitEachChild(node, visit, ctx);
31+
}
32+
return ts.visitEachChild(sourceFile, visit, ctx);
33+
};
34+
}
35+
36+
function isUsingDeclarationList(node: ts.Node): node is ts.VariableStatement {
37+
return ts.isVariableStatement(node) && (node.declarationList.flags & ts.NodeFlags.Using) !== 0;
38+
}
39+
40+
function transformBlockWithUsing(
41+
context: TransformationContext,
42+
statements: ts.NodeArray<ts.Statement> | ts.Statement[],
43+
block: ts.Block
44+
): [true, ts.Statement[]] | [false] {
45+
const newStatements: ts.Statement[] = [];
46+
47+
for (let i = 0; i < statements.length; i++) {
48+
const statement = statements[i];
49+
if (isUsingDeclarationList(statement)) {
50+
const isAwaitUsing = (statement.declarationList.flags & ts.NodeFlags.AwaitContext) !== 0;
51+
52+
if (isAwaitUsing) {
53+
importLuaLibFeature(context, LuaLibFeature.UsingAsync);
54+
} else {
55+
importLuaLibFeature(context, LuaLibFeature.Using);
56+
}
57+
58+
// Make declared using variables callback function parameters
59+
const variableNames = statement.declarationList.declarations.map(d =>
60+
ts.factory.createParameterDeclaration(undefined, undefined, d.name)
61+
);
62+
// Add this: void as first parameter
63+
variableNames.unshift(createThisVoidParameter(context.checker));
64+
65+
// Put all following statements in the callback body
66+
const followingStatements = statements.slice(i + 1);
67+
const [followingHasUsings, replacedFollowingStatements] = transformBlockWithUsing(
68+
context,
69+
followingStatements,
70+
block
71+
);
72+
const callbackBody = ts.factory.createBlock(
73+
followingHasUsings ? replacedFollowingStatements : followingStatements
74+
);
75+
76+
const callback = ts.factory.createFunctionExpression(
77+
undefined,
78+
undefined,
79+
undefined,
80+
undefined,
81+
variableNames,
82+
undefined,
83+
callbackBody
84+
);
85+
86+
// Replace using variable list with call to lualib function with callback and followed by all variable initializers
87+
const functionIdentifier = ts.factory.createIdentifier(isAwaitUsing ? "__TS__UsingAsync" : "__TS__Using");
88+
let call: ts.Expression = ts.factory.createCallExpression(
89+
functionIdentifier,
90+
[],
91+
[
92+
callback,
93+
...statement.declarationList.declarations.map(
94+
d => d.initializer ?? ts.factory.createIdentifier("unidentified")
95+
),
96+
]
97+
);
98+
99+
// If this is an 'await using ...', add an await statement here
100+
if (isAwaitUsing) {
101+
call = ts.factory.createAwaitExpression(call);
102+
}
103+
104+
if (ts.isBlock(block.parent) && block.parent.statements[block.parent.statements.length - 1] !== block) {
105+
// If this is a free-standing block in a function (not the last statement), dont return the value
106+
newStatements.push(ts.factory.createExpressionStatement(call));
107+
} else {
108+
newStatements.push(ts.factory.createReturnStatement(call));
109+
}
110+
111+
return [true, newStatements];
112+
} else {
113+
newStatements.push(statement);
114+
}
115+
}
116+
return [false];
117+
}
118+
119+
function createThisVoidParameter(checker: ts.TypeChecker) {
120+
const voidType = checker.typeToTypeNode(checker.getVoidType(), undefined, undefined);
121+
return ts.factory.createParameterDeclaration(
122+
undefined,
123+
undefined,
124+
ts.factory.createIdentifier("this"),
125+
undefined,
126+
voidType
127+
);
128+
}

src/transformation/visitors/function.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,15 @@ export function transformFunctionToExpression(
227227

228228
const type = context.checker.getTypeAtLocation(node);
229229
let functionContext: lua.Identifier | undefined;
230-
if (getFunctionContextType(context, type) !== ContextType.Void) {
230+
231+
const firstParam = node.parameters[0];
232+
const hasThisVoidParameter =
233+
firstParam &&
234+
ts.isIdentifier(firstParam.name) &&
235+
ts.identifierToKeywordKind(firstParam.name) === ts.SyntaxKind.ThisKeyword &&
236+
firstParam.type?.kind === ts.SyntaxKind.VoidKeyword;
237+
238+
if (!hasThisVoidParameter && getFunctionContextType(context, type) !== ContextType.Void) {
231239
if (ts.isArrowFunction(node)) {
232240
// dummy context for arrow functions with parameters
233241
if (node.parameters.length > 0) {

src/transformation/visitors/variable-declaration.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,8 @@ export function transformVariableDeclaration(
282282
}
283283

284284
export function checkVariableDeclarationList(context: TransformationContext, node: ts.VariableDeclarationList): void {
285-
if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) {
286-
const token = node.getFirstToken();
285+
if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using | ts.NodeFlags.AwaitUsing)) === 0) {
286+
const token = ts.getOriginalNode(node).getFirstToken();
287287
assert(token);
288288
context.diagnostics.push(unsupportedVarDeclaration(token));
289289
}

src/typescript-internal.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,6 @@ declare module "typescript" {
5959

6060
export function pathIsAbsolute(path: string): boolean;
6161
export function pathIsRelative(path: string): boolean;
62+
63+
export function setParent<T extends Node>(child: T, parent: T["parent"] | undefined): T;
6264
}

0 commit comments

Comments
 (0)