diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 6320bc99c..4fffc0cf3 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -51,6 +51,7 @@ export enum LuaLibFeature { DescriptorGet = "DescriptorGet", DescriptorSet = "DescriptorSet", Error = "Error", + ErrorObject = "ErrorObject", FunctionBind = "FunctionBind", Generator = "Generator", InstanceOf = "InstanceOf", diff --git a/src/lualib/ErrorObject.ts b/src/lualib/ErrorObject.ts new file mode 100644 index 000000000..7cf1d57fb --- /dev/null +++ b/src/lualib/ErrorObject.ts @@ -0,0 +1,18 @@ +// Lua 5.5 replaces a `nil` error object with a string ("") +// when an error propagates out of a protected call. This breaks `throw undefined` +// round-tripping through `pcall`. To preserve the original value, we route +// protected calls through `xpcall` with a message handler that wraps nil into +// this sentinel table (Lua 5.5 only mangles nil, not other values), and unwrap +// on the catch side. +// See: https://www.lua.org/manual/5.5/manual.html#8.1 +const ____TS__NilErrorObject: any = {}; + +export function __TS__WrapErrorObject(this: void, value: any): any { + if (value === undefined) return ____TS__NilErrorObject; + return value; +} + +export function __TS__UnwrapErrorObject(this: void, value: any): any { + if (value === ____TS__NilErrorObject) return undefined; + return value; +} diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 77067dda8..7e6378a21 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -4,7 +4,7 @@ import * as lua from "../../LuaAST"; import { FunctionVisitor, TransformationContext } from "../context"; import { unsupportedForTarget, unsupportedForTargetButOverrideAvailable } from "../utils/diagnostics"; import { createUnpackCall } from "../utils/lua-ast"; -import { transformLuaLibFunction } from "../utils/lualib"; +import { importLuaLibFeature, transformLuaLibFunction } from "../utils/lualib"; import { findScope, LoopContinued, Scope, ScopeType } from "../utils/scope"; import { isInAsyncFunction, isInGeneratorFunction } from "../utils/typescript"; import { wrapInAsyncAwaiter } from "./async-await"; @@ -171,8 +171,23 @@ export const transformTryStatement: FunctionVisitor = (statemen const returnedIdentifier = lua.createIdentifier("____hasReturned"); let returnCondition: lua.Expression | undefined; - const pCall = lua.createIdentifier("pcall"); - const tryCall = lua.createCallExpression(pCall, [lua.createFunctionExpression(tryBlock)]); + // On Lua 5.5 (and Universal, which must work on 5.5), a `nil` error object + // is replaced by a string when it propagates out of a protected call. To + // preserve `throw undefined` semantics, route through xpcall with a message + // handler that wraps nil into a sentinel, and unwrap before the user catch. + // See: https://www.lua.org/manual/5.5/manual.html#8.1 + const wrapErrorObjects = + context.options.luaTarget === LuaTarget.Lua55 || context.options.luaTarget === LuaTarget.Universal; + if (wrapErrorObjects) { + importLuaLibFeature(context, LuaLibFeature.ErrorObject); + } + + const tryCall = wrapErrorObjects + ? lua.createCallExpression(lua.createIdentifier("xpcall"), [ + lua.createFunctionExpression(tryBlock), + lua.createIdentifier("__TS__WrapErrorObject"), + ]) + : lua.createCallExpression(lua.createIdentifier("pcall"), [lua.createFunctionExpression(tryBlock)]); if (statement.catchClause && statement.catchClause.block.statements.length > 0) { // try with catch @@ -192,9 +207,14 @@ export const transformTryStatement: FunctionVisitor = (statemen } result.push(lua.createVariableDeclarationStatement(tryReturnIdentifiers, tryCall)); + const catchArg = wrapErrorObjects + ? lua.createCallExpression(lua.createIdentifier("__TS__UnwrapErrorObject"), [ + lua.cloneIdentifier(returnedIdentifier), + ]) + : lua.cloneIdentifier(returnedIdentifier); const catchCall = lua.createCallExpression( catchIdentifier, - statement.catchClause.variableDeclaration ? [lua.cloneIdentifier(returnedIdentifier)] : [] + statement.catchClause.variableDeclaration ? [catchArg] : [] ); const catchCallStatement = hasReturn ? lua.createAssignmentStatement( diff --git a/test/util.ts b/test/util.ts index 1e191fb28..a82ca3bf5 100644 --- a/test/util.ts +++ b/test/util.ts @@ -42,11 +42,15 @@ function getLuaBindingsForVersion(target: tstl.LuaTarget): { lauxlib: LauxLib; l const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.53"); return { lauxlib, lua, lualib }; } + if (target === tstl.LuaTarget.Lua54) { + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.54"); + return { lauxlib, lua, lualib }; + } if (target === tstl.LuaTarget.LuaJIT) { throw Error("Can't use executeLua() or expectToMatchJsResult() with LuaJIT as target!"); } - const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.54"); + const { lauxlib, lua, lualib } = require("lua-wasm-bindings/dist/lua.55"); return { lauxlib, lua, lualib }; }