diff --git a/src/lualib/Promise.ts b/src/lualib/Promise.ts index c8fdea117..76f119c38 100644 --- a/src/lualib/Promise.ts +++ b/src/lualib/Promise.ts @@ -70,13 +70,16 @@ class __TS__Promise implements Promise { onFulfilled?: FulfillCallback, onRejected?: RejectCallback ): Promise { - const { promise, resolve, reject } = __TS__PromiseDeferred(); + const { promise, resolve, reject } = __TS__PromiseDeferred(); + + 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); } @@ -89,13 +92,23 @@ class __TS__Promise implements Promise { 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; } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch public catch(onRejected?: (reason: any) => TResult | PromiseLike): Promise { diff --git a/src/transformation/utils/typescript/nodes.ts b/src/transformation/utils/typescript/nodes.ts index 3007c4d7d..ef6cec08f 100644 --- a/src/transformation/utils/typescript/nodes.ts +++ b/src/transformation/utils/typescript/nodes.ts @@ -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 { @@ -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! */ diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 05601d8c8..2b58428b2 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -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"; @@ -11,6 +14,18 @@ import { createReturnStatement } from "./return"; export const transformTryStatement: FunctionVisitor = (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"); diff --git a/src/transformation/visitors/return.ts b/src/transformation/visitors/return.ts index 6a5b1bbb8..1bd1bb0fe 100644 --- a/src/transformation/visitors/return.ts +++ b/src/transformation/visitors/return.ts @@ -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, @@ -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; diff --git a/test/unit/builtins/async-await.spec.ts b/test/unit/builtins/async-await.spec.ts index 668551bd0..c08514236 100644 --- a/test/unit/builtins/async-await.spec.ts +++ b/test/unit/builtins/async-await.spec.ts @@ -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 = ` @@ -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 { try { @@ -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 { try { @@ -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; @@ -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]), + } + ); }); diff --git a/test/unit/builtins/promise.spec.ts b/test/unit/builtins/promise.spec.ts index 4a22f7514..60e30dcf1 100644 --- a/test/unit/builtins/promise.spec.ts +++ b/test/unit/builtins/promise.spec.ts @@ -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` diff --git a/test/unit/functions/generators.spec.ts b/test/unit/functions/generators.spec.ts index 7065aecf6..03b7f3bfd 100644 --- a/test/unit/functions/generators.spec.ts +++ b/test/unit/functions/generators.spec.ts @@ -1,3 +1,5 @@ +import { LuaTarget } from "../../../src/CompilerOptions"; +import { unsupportedForTarget } from "../../../src/transformation/utils/diagnostics"; import * as util from "../../util"; test("generator parameters", () => { @@ -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]), + } +); diff --git a/test/util.ts b/test/util.ts index 7752496a6..7d5ea6b95 100644 --- a/test/util.ts +++ b/test/util.ts @@ -65,6 +65,19 @@ export function testEachVersion( } } +export function expectEachVersionExceptJit( + expectation: (builder: T) => void +): Record 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();