Skip to content
21 changes: 17 additions & 4 deletions src/lualib/Promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ class __TS__Promise<T> implements Promise<T> {
onFulfilled?: FulfillCallback<T, TResult1>,
onRejected?: RejectCallback<TResult2>
): Promise<TResult1 | TResult2> {
const { promise, resolve, reject } = __TS__PromiseDeferred<TResult1 | TResult2>();
const { promise, resolve, reject } = __TS__PromiseDeferred<T | TResult1 | TResult2>();

const isFulfilled = this.state === __TS__PromiseState.Fulfilled;
const isRejected = this.state === __TS__PromiseState.Rejected;

if (onFulfilled) {
const internalCallback = this.createPromiseResolvingCallback(onFulfilled, resolve, reject);
this.fulfilledCallbacks.push(internalCallback);

if (this.state === __TS__PromiseState.Fulfilled) {
if (isFulfilled) {
// If promise already resolved, immediately call callback
internalCallback(this.value);
}
Expand All @@ -89,13 +92,23 @@ class __TS__Promise<T> implements Promise<T> {
const internalCallback = this.createPromiseResolvingCallback(onRejected, resolve, reject);
this.rejectedCallbacks.push(internalCallback);

if (this.state === __TS__PromiseState.Rejected) {
if (isRejected) {
// If promise already rejected, immediately call callback
internalCallback(this.rejectionReason);
}
}

return promise;
if (isFulfilled) {
// If promise already resolved, also resolve returned promise
resolve(this.value);
}

if (isRejected) {
// If promise already rejected, also reject returned promise
reject(this.rejectionReason);
}

return promise as Promise<TResult1 | TResult2>;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
public catch<TResult = never>(onRejected?: (reason: any) => TResult | PromiseLike<TResult>): Promise<T | TResult> {
Expand Down
21 changes: 21 additions & 0 deletions src/transformation/utils/typescript/nodes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as ts from "typescript";
import { findFirstNodeAbove } from ".";
import { TransformationContext } from "../../context";

export function isAssignmentPattern(node: ts.Node): node is ts.AssignmentPattern {
Expand All @@ -25,6 +26,26 @@ export function isInDestructingAssignment(node: ts.Node): boolean {
);
}

export function isInAsyncFunction(node: ts.Node): boolean {
// Check if node is in function declaration with `async`
const declaration = findFirstNodeAbove(node, ts.isFunctionLike);
if (!declaration) {
return false;
}

return declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
}

export function isInGeneratorFunction(node: ts.Node): boolean {
// Check if node is in function declaration with `async`
const declaration = findFirstNodeAbove(node, ts.isFunctionDeclaration);
if (!declaration) {
return false;
}

return declaration.asteriskToken !== undefined;
}

/**
* Quite hacky, avoid unless absolutely necessary!
*/
Expand Down
15 changes: 15 additions & 0 deletions src/transformation/visitors/errors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import * as ts from "typescript";
import { LuaTarget } from "../..";
import * as lua from "../../LuaAST";
import { FunctionVisitor } from "../context";
import { unsupportedForTarget } from "../utils/diagnostics";
import { createUnpackCall } from "../utils/lua-ast";
import { ScopeType } from "../utils/scope";
import { isInAsyncFunction, isInGeneratorFunction } from "../utils/typescript";
import { transformScopeBlock } from "./block";
import { transformIdentifier } from "./identifier";
import { isInMultiReturnFunction } from "./language-extensions/multi";
Expand All @@ -11,6 +14,18 @@ import { createReturnStatement } from "./return";
export const transformTryStatement: FunctionVisitor<ts.TryStatement> = (statement, context) => {
const [tryBlock, tryScope] = transformScopeBlock(context, statement.tryBlock, ScopeType.Try);

if (context.options.luaTarget === LuaTarget.Lua51 && isInAsyncFunction(statement)) {
context.diagnostics.push(unsupportedForTarget(statement, "try/catch inside async functions", LuaTarget.Lua51));
return tryBlock.statements;
}

if (context.options.luaTarget === LuaTarget.Lua51 && isInGeneratorFunction(statement)) {
context.diagnostics.push(
unsupportedForTarget(statement, "try/catch inside generator functions", LuaTarget.Lua51)
);
return tryBlock.statements;
}

const tryResultIdentifier = lua.createIdentifier("____try");
const returnValueIdentifier = lua.createIdentifier("____returnValue");

Expand Down
12 changes: 1 addition & 11 deletions src/transformation/visitors/return.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
canBeMultiReturnType,
} from "./language-extensions/multi";
import { invalidMultiFunctionReturnType } from "../utils/diagnostics";
import { findFirstNodeAbove } from "../utils/typescript";
import { isInAsyncFunction } from "../utils/typescript";

function transformExpressionsInReturn(
context: TransformationContext,
Expand Down Expand Up @@ -96,16 +96,6 @@ export function createReturnStatement(
return lua.createReturnStatement(results, node);
}

function isInAsyncFunction(node: ts.Node): boolean {
// Check if node is in function declaration with `async`
const declaration = findFirstNodeAbove(node, ts.isFunctionLike);
if (!declaration) {
return false;
}

return declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
}

function isInTryCatch(context: TransformationContext): boolean {
// Check if context is in a try or catch
let insideTryCatch = false;
Expand Down
48 changes: 35 additions & 13 deletions test/unit/builtins/async-await.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ModuleKind, ScriptTarget } from "typescript";
import { awaitMustBeInAsyncFunction } from "../../../src/transformation/utils/diagnostics";
import { LuaTarget } from "../../../src";
import { awaitMustBeInAsyncFunction, unsupportedForTarget } from "../../../src/transformation/utils/diagnostics";
import * as util from "../../util";

const promiseTestLib = `
Expand Down Expand Up @@ -385,8 +386,9 @@ test("async function can forward varargs", () => {

// https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1105
describe("try/catch in async function", () => {
test("await inside try/catch returns inside async function", () => {
util.testModule`
util.testEachVersion(
"await inside try/catch returns inside async function",
() => util.testModule`
export let result = 0;
async function foo(): Promise<number> {
try {
Expand All @@ -398,11 +400,17 @@ describe("try/catch in async function", () => {
foo().then(value => {
result = value;
});
`.expectToEqual({ result: 4 });
});
`,
// Cannot execute LuaJIT with test runner
{
...util.expectEachVersionExceptJit(builder => builder.expectToEqual({ result: 4 })),
[LuaTarget.Lua51]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]),
}
);

test("await inside try/catch throws inside async function", () => {
util.testModule`
util.testEachVersion(
"await inside try/catch throws inside async function",
() => util.testModule`
export let reason = "";
async function foo(): Promise<number> {
try {
Expand All @@ -414,11 +422,19 @@ describe("try/catch in async function", () => {
foo().catch(e => {
reason = e;
});
`.expectToEqual({ reason: "an error occurred in the async function: test error" });
});
`,
{
...util.expectEachVersionExceptJit(builder =>
builder.expectToEqual({ reason: "an error occurred in the async function: test error" })
),
[LuaTarget.Lua51]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]),
}
);

test("await inside try/catch deferred rejection uses catch clause", () => {
util.testModule`
util.testEachVersion(
"await inside try/catch deferred rejection uses catch clause",
() =>
util.testModule`
export let reason = "";
let reject: (reason: string) => void;

Expand All @@ -433,6 +449,12 @@ describe("try/catch in async function", () => {
reason = e;
});
reject("test error");
`.expectToEqual({ reason: "an error occurred in the async function: test error" });
});
`,
{
...util.expectEachVersionExceptJit(builder =>
builder.expectToEqual({ reason: "an error occurred in the async function: test error" })
),
[LuaTarget.Lua51]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]),
}
);
});
42 changes: 42 additions & 0 deletions test/unit/builtins/promise.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,48 @@ test("promise is instanceof promise", () => {
util.testExpression`Promise.resolve(4) instanceof Promise`.expectToMatchJsResult();
});

test("chained then on resolved promise", () => {
util.testFunction`
Promise.resolve("result1").then(undefined, () => {}).then(value => log(value));
Promise.resolve("result2").then(value => "then1", () => {}).then(value => log(value));
Promise.resolve("result3").then(value => undefined, () => {}).then(value => log(value ?? "undefined"));
Promise.resolve("result4").then(value => "then2").then(value => [value, "then3"]).then(([v1, v2]) => log(v1, v2));

return allLogs;
`
.setTsHeader(promiseTestLib)
.expectToEqual(["result1", "then1", "undefined", "then2", "then3"]);
});

test("chained catch on rejected promise", () => {
util.testFunction`
Promise.reject("reason1").then(() => {}).then(v => log("resolved", v), reason => log("rejected", reason));
Promise.reject("reason2").then(() => {}, () => "reason3").then(v => log("resolved", v));
Promise.reject("reason4").then(() => {}, () => undefined).then(v => log("resolved", v ?? "undefined"));

return allLogs;
`
.setTsHeader(promiseTestLib)
.expectToEqual(["rejected", "reason1", "resolved", "reason3", "resolved", "undefined"]);
});

// Issue 2 from https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1105
test("catch after then catches rejected promise", () => {
util.testFunction`
Promise.reject('test error')
.then(result => {
log("then", result);
})
.catch(e => {
log("catch", e);
})

return allLogs;
`
.setTsHeader(promiseTestLib)
.expectToEqual(["catch", "test error"]);
});

describe("Promise.all", () => {
test("resolves once all arguments are resolved", () => {
util.testFunction`
Expand Down
21 changes: 21 additions & 0 deletions test/unit/functions/generators.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { LuaTarget } from "../../../src/CompilerOptions";
import { unsupportedForTarget } from "../../../src/transformation/utils/diagnostics";
import * as util from "../../util";

test("generator parameters", () => {
Expand Down Expand Up @@ -147,3 +149,22 @@ test("hoisting", () => {
}
`.expectToMatchJsResult();
});

util.testEachVersion(
"generator yield inside try/catch",
() => util.testFunction`
function* generator() {
try {
yield 4;
} catch {
throw "something went wrong";
}
}
return generator().next();
`,
// Cannot execute LuaJIT with test runner
{
...util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()),
[LuaTarget.Lua51]: builder => builder.expectToHaveDiagnostics([unsupportedForTarget.code]),
}
);
13 changes: 13 additions & 0 deletions test/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,19 @@ export function testEachVersion<T extends TestBuilder>(
}
}

export function expectEachVersionExceptJit<T>(
expectation: (builder: T) => void
): Record<tstl.LuaTarget, ((builder: T) => void) | boolean> {
return {
[tstl.LuaTarget.Universal]: expectation,
[tstl.LuaTarget.Lua51]: expectation,
[tstl.LuaTarget.Lua52]: expectation,
[tstl.LuaTarget.Lua53]: expectation,
[tstl.LuaTarget.Lua54]: expectation,
[tstl.LuaTarget.LuaJIT]: false, // Exclude JIT
};
}

const memoize: MethodDecorator = (_target, _propertyKey, descriptor) => {
const originalFunction = descriptor.value as any;
const memoized = new WeakMap();
Expand Down