Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export enum LuaLibFeature {
SymbolRegistry = "SymbolRegistry",
TypeOf = "TypeOf",
Unpack = "Unpack",
Using = "Using",
UsingAsync = "UsingAsync",
}

export interface LuaLibFeatureInfo {
Expand Down
2 changes: 2 additions & 0 deletions src/lualib/Symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export function __TS__Symbol(this: void, description?: string | number): symbol
}

export const Symbol = {
asyncDispose: __TS__Symbol("Symbol.asyncDispose"),
dispose: __TS__Symbol("Symbol.dispose"),
iterator: __TS__Symbol("Symbol.iterator"),
hasInstance: __TS__Symbol("Symbol.hasInstance"),

Expand Down
22 changes: 22 additions & 0 deletions src/lualib/Using.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export function __TS__Using<TArgs extends Disposable[], TReturn>(
this: undefined,
cb: (...args: TArgs) => TReturn,
...args: TArgs
): TReturn {
let thrownError;
const [ok, result] = xpcall(
() => cb(...args),
err => (thrownError = err)
);

const argArray = [...args];
for (let i = argArray.length - 1; i >= 0; i--) {
argArray[i][Symbol.dispose]();
}

if (!ok) {
throw thrownError;
}

return result;
}
27 changes: 27 additions & 0 deletions src/lualib/UsingAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export async function __TS__UsingAsync<TArgs extends Array<Disposable | AsyncDisposable>, TReturn>(
this: undefined,
cb: (...args: TArgs) => TReturn,
...args: TArgs
): Promise<TReturn> {
let thrownError;
const [ok, result] = xpcall(
() => cb(...args),
err => (thrownError = err)
);

const argArray = [...args];
for (let i = argArray.length - 1; i >= 0; i--) {
if (Symbol.dispose in argArray[i]) {
(argArray[i] as Disposable)[Symbol.dispose]();
}
if (Symbol.asyncDispose in argArray[i]) {
await (argArray[i] as AsyncDisposable)[Symbol.asyncDispose]();
}
}

if (!ok) {
throw thrownError;
}

return result;
}
9 changes: 8 additions & 1 deletion src/transformation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as lua from "../LuaAST";
import { getOrUpdate } from "../utils";
import { ObjectVisitor, TransformationContext, VisitorMap, Visitors } from "./context";
import { standardVisitors } from "./visitors";
import { usingTransformer } from "./pre-transformers/using-transformer";

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

export function transformSourceFile(program: ts.Program, sourceFile: ts.SourceFile, visitorMap: VisitorMap) {
const context = new TransformationContext(program, sourceFile, visitorMap);
const [file] = context.transformNode(sourceFile) as [lua.File];

// TS -> TS pre-transformation
const preTransformers = [usingTransformer(context)];
const result = ts.transform(sourceFile, preTransformers);

// TS -> Lua transformation
const [file] = context.transformNode(result.transformed[0]) as [lua.File];

return { file, diagnostics: context.diagnostics };
}
128 changes: 128 additions & 0 deletions src/transformation/pre-transformers/using-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import * as ts from "typescript";
import { TransformationContext } from "../context";
import { LuaLibFeature, importLuaLibFeature } from "../utils/lualib";

export function usingTransformer(context: TransformationContext): ts.TransformerFactory<ts.SourceFile> {
return ctx => sourceFile => {
function visit(node: ts.Node): ts.Node {
if (ts.isBlock(node)) {
const [hasUsings, newStatements] = transformBlockWithUsing(context, node.statements, node);
if (hasUsings) {
// Recurse visitor into updated block to find further usings
const updatedBlock = ts.factory.updateBlock(node, newStatements);
const result = ts.visitEachChild(updatedBlock, visit, ctx);

// Set all the synthetic node parents to something that makes sense
const parent: ts.Node[] = [updatedBlock];
function setParent(node2: ts.Node): ts.Node {
ts.setParent(node2, parent[parent.length - 1]);
parent.push(node2);
ts.visitEachChild(node2, setParent, ctx);
parent.push();
return node2;
}
ts.visitEachChild(updatedBlock, setParent, ctx);
ts.setParent(updatedBlock, node.parent);

return result;
}
}
return ts.visitEachChild(node, visit, ctx);
}
return ts.visitEachChild(sourceFile, visit, ctx);
};
}

function isUsingDeclarationList(node: ts.Node): node is ts.VariableStatement {
return ts.isVariableStatement(node) && (node.declarationList.flags & ts.NodeFlags.Using) !== 0;
}

function transformBlockWithUsing(
context: TransformationContext,
statements: ts.NodeArray<ts.Statement> | ts.Statement[],
block: ts.Block
): [true, ts.Statement[]] | [false] {
const newStatements: ts.Statement[] = [];

for (let i = 0; i < statements.length; i++) {
const statement = statements[i];
if (isUsingDeclarationList(statement)) {
const isAwaitUsing = (statement.declarationList.flags & ts.NodeFlags.AwaitContext) !== 0;

if (isAwaitUsing) {
importLuaLibFeature(context, LuaLibFeature.UsingAsync);
} else {
importLuaLibFeature(context, LuaLibFeature.Using);
}

// Make declared using variables callback function parameters
const variableNames = statement.declarationList.declarations.map(d =>
ts.factory.createParameterDeclaration(undefined, undefined, d.name)
);
// Add this: void as first parameter
variableNames.unshift(createThisVoidParameter(context.checker));

// Put all following statements in the callback body
const followingStatements = statements.slice(i + 1);
const [followingHasUsings, replacedFollowingStatements] = transformBlockWithUsing(
context,
followingStatements,
block
);
const callbackBody = ts.factory.createBlock(
followingHasUsings ? replacedFollowingStatements : followingStatements
);

const callback = ts.factory.createFunctionExpression(
undefined,
undefined,
undefined,
undefined,
variableNames,
undefined,
callbackBody
);

// Replace using variable list with call to lualib function with callback and followed by all variable initializers
const functionIdentifier = ts.factory.createIdentifier(isAwaitUsing ? "__TS__UsingAsync" : "__TS__Using");
let call: ts.Expression = ts.factory.createCallExpression(
functionIdentifier,
[],
[
callback,
...statement.declarationList.declarations.map(
d => d.initializer ?? ts.factory.createIdentifier("unidentified")
),
]
);

// If this is an 'await using ...', add an await statement here
if (isAwaitUsing) {
call = ts.factory.createAwaitExpression(call);
}

if (ts.isBlock(block.parent) && block.parent.statements[block.parent.statements.length - 1] !== block) {
// If this is a free-standing block in a function (not the last statement), dont return the value
newStatements.push(ts.factory.createExpressionStatement(call));
} else {
newStatements.push(ts.factory.createReturnStatement(call));
}

return [true, newStatements];
} else {
newStatements.push(statement);
}
}
return [false];
}

function createThisVoidParameter(checker: ts.TypeChecker) {
const voidType = checker.typeToTypeNode(checker.getVoidType(), undefined, undefined);
return ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier("this"),
undefined,
voidType
);
}
10 changes: 9 additions & 1 deletion src/transformation/visitors/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,15 @@ export function transformFunctionToExpression(

const type = context.checker.getTypeAtLocation(node);
let functionContext: lua.Identifier | undefined;
if (getFunctionContextType(context, type) !== ContextType.Void) {

const firstParam = node.parameters[0];
const hasThisVoidParameter =
firstParam &&
ts.isIdentifier(firstParam.name) &&
ts.identifierToKeywordKind(firstParam.name) === ts.SyntaxKind.ThisKeyword &&
firstParam.type?.kind === ts.SyntaxKind.VoidKeyword;

if (!hasThisVoidParameter && getFunctionContextType(context, type) !== ContextType.Void) {
if (ts.isArrowFunction(node)) {
// dummy context for arrow functions with parameters
if (node.parameters.length > 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/transformation/visitors/variable-declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,8 @@ export function transformVariableDeclaration(
}

export function checkVariableDeclarationList(context: TransformationContext, node: ts.VariableDeclarationList): void {
if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0) {
const token = node.getFirstToken();
if ((node.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const | ts.NodeFlags.Using | ts.NodeFlags.AwaitUsing)) === 0) {
const token = ts.getOriginalNode(node).getFirstToken();
assert(token);
context.diagnostics.push(unsupportedVarDeclaration(token));
}
Expand Down
2 changes: 2 additions & 0 deletions src/typescript-internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,6 @@ declare module "typescript" {

export function pathIsAbsolute(path: string): boolean;
export function pathIsRelative(path: string): boolean;

export function setParent<T extends Node>(child: T, parent: T["parent"] | undefined): T;
}
Loading