From edb829e39642d72b5f6c0450177b7de214594697 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 13:09:40 +0000 Subject: [PATCH 1/5] test: run lua 5.5 tests on the lua 5.5 wasm binding --- test/util.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 }; } From 769702535b9487477f316c2be4636734c8d08afc Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 13:20:28 +0000 Subject: [PATCH 2/5] fix: wrap nil error objects on lua 5.5 to preserve throw undefined across pcall --- src/LuaLib.ts | 1 + src/lualib/ErrorObject.ts | 17 +++++++++++++++++ src/transformation/visitors/errors.ts | 27 +++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/lualib/ErrorObject.ts 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..220f42917 --- /dev/null +++ b/src/lualib/ErrorObject.ts @@ -0,0 +1,17 @@ +// 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. +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..90345c805 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,22 @@ 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. + 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 +206,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( From 94d020e579fe7a30aca302b8e58df295b60ce585 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 13:39:17 +0000 Subject: [PATCH 3/5] docs: link lua 5.5 manual section 8.1 from error wrapping comments --- src/lualib/ErrorObject.ts | 1 + src/transformation/visitors/errors.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/lualib/ErrorObject.ts b/src/lualib/ErrorObject.ts index 220f42917..7cf1d57fb 100644 --- a/src/lualib/ErrorObject.ts +++ b/src/lualib/ErrorObject.ts @@ -4,6 +4,7 @@ // 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 { diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 90345c805..7e6378a21 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -175,6 +175,7 @@ export const transformTryStatement: FunctionVisitor = (statemen // 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) { From 11b192a1b67e6d172dcb647a24645906edb8c8fa Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 20:57:06 +0000 Subject: [PATCH 4/5] what happens if we don't wrap? --- src/transformation/visitors/errors.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 7e6378a21..4f4ffb080 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -176,8 +176,9 @@ export const transformTryStatement: FunctionVisitor = (statemen // 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; + // const wrapErrorObjects = + // context.options.luaTarget === LuaTarget.Lua55 || context.options.luaTarget === LuaTarget.Universal; + const wrapErrorObjects = false; if (wrapErrorObjects) { importLuaLibFeature(context, LuaLibFeature.ErrorObject); } From 56281f2ebe57b24cebb99c60c2358e8852ed8335 Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sat, 2 May 2026 20:57:27 +0000 Subject: [PATCH 5/5] revert to wrapping --- src/transformation/visitors/errors.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/transformation/visitors/errors.ts b/src/transformation/visitors/errors.ts index 4f4ffb080..7e6378a21 100644 --- a/src/transformation/visitors/errors.ts +++ b/src/transformation/visitors/errors.ts @@ -176,9 +176,8 @@ export const transformTryStatement: FunctionVisitor = (statemen // 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; - const wrapErrorObjects = false; + const wrapErrorObjects = + context.options.luaTarget === LuaTarget.Lua55 || context.options.luaTarget === LuaTarget.Universal; if (wrapErrorObjects) { importLuaLibFeature(context, LuaLibFeature.ErrorObject); }