From 523849964b2b88eb1d3f619a259f7a205a9d203c Mon Sep 17 00:00:00 2001 From: Aleksandr Krivoshchekov Date: Sun, 28 Jun 2026 05:43:02 +0100 Subject: [PATCH 1/3] Fix try/catch/finally flow Closes: #1137 --- src/transformation/visitors/errors.ts | 55 ++++++++++++++++++++------- test/unit/error.spec.ts | 29 +++++++++----- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 77067dda8..ea94b865c 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -165,6 +165,7 @@ export const transformTryStatement: FunctionVisitor = (statemen const tryResultIdentifier = lua.createIdentifier("____try"); const returnValueIdentifier = lua.createIdentifier("____returnValue"); + const rethrowIdentifier = lua.createIdentifier("____rethrow"); const result: lua.Statement[] = []; @@ -191,6 +192,9 @@ export const transformTryStatement: FunctionVisitor = (statemen } } result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + if (statement.finallyBlock) { + result.push(lua.createVariableDeclarationStatement(rethrowIdentifier)); + } const catchCall = lua.createCallExpression( catchIdentifier, @@ -198,13 +202,26 @@ export const transformTryStatement: FunctionVisitor = (statemen ); const catchCallStatement = hasReturn ? lua.createAssignmentStatement( - [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], - catchCall - ) + [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], + catchCall + ) : lua.createExpressionStatement(catchCall); + const catchCallFunction = lua.createFunctionExpression(lua.createBlock([catchCallStatement])); + + const tryCatchCall = lua.createCallExpression(pCall, [catchCallFunction]); + + const tryCatchCallStatement = lua.createAssignmentStatement( + [lua.cloneIdentifier(tryResultIdentifier), lua.cloneIdentifier(rethrowIdentifier)], + tryCatchCall + ); + const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([catchCallStatement]))); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock( + statement.finallyBlock + ? [tryCatchCallStatement] + : [catchCallStatement] + ))); } else if (tryScope.functionReturned) { // try with return, but no catch // returnedIdentifier = lua.createIdentifier("____returned"); @@ -230,21 +247,31 @@ export const transformTryStatement: FunctionVisitor = (statemen result.push(...context.transformStatements(statement.finallyBlock)); } - // Re-throw error if try had no catch but had a finally. + // Re-throw error if try had a finally. // On pcall failure the error is the second return value, which lands in - // ____hasReturned (when functionReturned) or ____error (otherwise). - if (!statement.catchClause && statement.finallyBlock) { + // ____hasReturned (when functionReturned), ____rethrow + // (catch throw/re-throw), or ____error (otherwise). + if (statement.finallyBlock) { const notTryCondition = lua.createUnaryExpression( lua.cloneIdentifier(tryResultIdentifier), lua.SyntaxKind.NotOperator ); - const errorIdentifier = tryScope.functionReturned - ? lua.cloneIdentifier(returnedIdentifier) - : lua.createIdentifier("____error"); - const rethrow = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) - ); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + + if (!statement.catchClause) { + const errorIdentifier = tryScope.functionReturned + ? lua.cloneIdentifier(returnedIdentifier) + : lua.createIdentifier("____error"); + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [errorIdentifier, lua.createNumericLiteral(0)]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } else if (statement.catchClause.block.statements.length > 0) { + // Re-throw is possible only from non-empty blocks. + const rethrow = lua.createExpressionStatement( + lua.createCallExpression(lua.createIdentifier("error"), [rethrowIdentifier, lua.createNumericLiteral(0)]) + ); + result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); + } } if (returnCondition && returnedIdentifier) { diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 8204cc36a..f7056655f 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -7,31 +7,40 @@ test("throwString", () => { `.expectToEqual(new util.ExecutionError("Some Error")); }); -// TODO: Finally does not behave like it should, see #1137 -// eslint-disable-next-line jest/no-disabled-tests -test.skip.each([0, 1, 2])("re-throw (%p)", i => { +test.each([ + { innerTry: false, innerFinally: false, outerFinally: false }, + { innerTry: false, innerFinally: false, outerFinally: true }, + { innerTry: false, innerFinally: true, outerFinally: false }, + { innerTry: false, innerFinally: true, outerFinally: true }, + { innerTry: true, innerFinally: false, outerFinally: false }, + { innerTry: true, innerFinally: false, outerFinally: true }, + { innerTry: true, innerFinally: true, outerFinally: false }, + { innerTry: true, innerFinally: true, outerFinally: true }, +])("re-throw (%j)", o => { util.testFunction` - const i: number = ${i}; + const innerTry: boolean = ${o.innerTry}; + const innerFinally: boolean = ${o.innerFinally}; + const outerFinally: boolean = ${o.outerFinally}; function foo() { try { try { - if (i === 0) { throw "z"; } + if (innerTry) { throw "inner.try"; } } catch (e) { - throw "a"; + throw (e as string) + "->" + "inner.catch"; } finally { - if (i === 1) { throw "b"; } + if (innerFinally) { throw "inner.finally"; } } } catch (e) { - throw (e as string).toUpperCase(); + throw (e as string) + "->" + "outer.catch"; } finally { - throw "C"; + if (outerFinally) { throw "outer.finally"; } } } let result: string = "x"; try { foo(); } catch (e) { - result = (e as string)[(e as string).length - 1]; + result = e as string; } return result; `.expectToMatchJsResult(); From 5f258527929f6c884a926d4afdce25acfa26817c Mon Sep 17 00:00:00 2001 From: Aleksandr Krivoshchekov Date: Sun, 28 Jun 2026 05:55:31 +0100 Subject: [PATCH 2/3] Fix comment --- src/transformation/visitors/errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index ea94b865c..470ccea3d 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -249,8 +249,7 @@ export const transformTryStatement: FunctionVisitor = (statemen // Re-throw error if try had a finally. // On pcall failure the error is the second return value, which lands in - // ____hasReturned (when functionReturned), ____rethrow - // (catch throw/re-throw), or ____error (otherwise). + // ____hasReturned (when functionReturned) or ____error (otherwise). if (statement.finallyBlock) { const notTryCondition = lua.createUnaryExpression( lua.cloneIdentifier(tryResultIdentifier), From c8d8148b8e3a360e1f246af35820e9cfef1b538a Mon Sep 17 00:00:00 2001 From: Aleksandr Krivoshchekov Date: Mon, 29 Jun 2026 03:10:23 +0100 Subject: [PATCH 3/3] Run prettier --- src/transformation/visitors/errors.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 470ccea3d..a070caac6 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -202,9 +202,9 @@ export const transformTryStatement: FunctionVisitor = (statemen ); const catchCallStatement = hasReturn ? lua.createAssignmentStatement( - [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], - catchCall - ) + [lua.cloneIdentifier(returnedIdentifier), lua.cloneIdentifier(returnValueIdentifier)], + catchCall + ) : lua.createExpressionStatement(catchCall); const catchCallFunction = lua.createFunctionExpression(lua.createBlock([catchCallStatement])); @@ -217,11 +217,12 @@ export const transformTryStatement: FunctionVisitor = (statemen ); const notTryCondition = lua.createUnaryExpression(tryResultIdentifier, lua.SyntaxKind.NotOperator); - result.push(lua.createIfStatement(notTryCondition, lua.createBlock( - statement.finallyBlock - ? [tryCatchCallStatement] - : [catchCallStatement] - ))); + result.push( + lua.createIfStatement( + notTryCondition, + lua.createBlock(statement.finallyBlock ? [tryCatchCallStatement] : [catchCallStatement]) + ) + ); } else if (tryScope.functionReturned) { // try with return, but no catch // returnedIdentifier = lua.createIdentifier("____returned"); @@ -267,7 +268,10 @@ export const transformTryStatement: FunctionVisitor = (statemen } else if (statement.catchClause.block.statements.length > 0) { // Re-throw is possible only from non-empty blocks. const rethrow = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("error"), [rethrowIdentifier, lua.createNumericLiteral(0)]) + lua.createCallExpression(lua.createIdentifier("error"), [ + rethrowIdentifier, + lua.createNumericLiteral(0), + ]) ); result.push(lua.createIfStatement(notTryCondition, lua.createBlock([rethrow]))); }