import { expect, test } from "vitest"; import { isPromise } from "./rp.ts"; import { parse } from "./parse.ts"; import { evalStatement } from "./eval.ts"; import { LuaEnv, LuaRuntimeError, LuaStackFrame } from "./runtime.ts"; import { luaBuildStandardEnv } from "./stdlib.ts"; async function evalBlock(code: string, env?: LuaEnv): Promise { const ast = parse(code); const G = luaBuildStandardEnv(); const base = env ?? new LuaEnv(G); const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx); const r = evalStatement(ast, base, sf, false); if (isPromise(r)) { await r; } return base; } async function runAndCatch(code: string, ref = "close_attribute.lua") { const ast = parse(code, { ref }); const G = luaBuildStandardEnv(); const env = new LuaEnv(G); const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx); try { const r = evalStatement(ast, env, sf, false); if (isPromise(r)) { await r; } throw new Error("Expected error but evaluation succeeded"); } catch (e: unknown) { return { e, code, ref }; } } async function runAndCatchEnv(code: string, ref = "close_attribute.lua") { const ast = parse(code, { ref }); const G = luaBuildStandardEnv(); const env = new LuaEnv(G); const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx); let err: unknown = null; try { const r = evalStatement(ast, env, sf, false); if (isPromise(r)) { await r; } } catch (e: unknown) { err = e; } return { err, env }; } // 1. parsing and static validation test("close: parse ok", () => { parse( ` local x = {} `, { ref: "close_parse.lua" }, ); }); test("close: parse ok without init", () => { parse( ` do local x end `, { ref: "close_no_init.lua" }, ); }); test("close: local list only one", () => { try { parse( ` do local a, b = {}, {} end `, { ref: "multi_close.lua" }, ); throw new Error("Expected parse error"); } catch (e) { expect(String((e as any)?.message ?? e)).toContain(""); expect(String((e as any)?.message ?? e)).toContain("local list"); } }); test("close: parse ok", () => { parse( ` do local x = 1 end `, { ref: "const_parse.lua" }, ); }); test("close: and parse ok", () => { parse( ` do local x = 1 local y = setmetatable({}, { __close = function() end }) end `, { ref: "const_and_close_parse.lua" }, ); }); test("close: invalid goto into scope", () => { try { parse( ` do goto L local x = setmetatable({}, { __close = function() end }) ::L:: end `, { ref: "goto_into_scope.lua" }, ); throw new Error("Expected parse error"); } catch (e) { expect(String((e as any)?.message ?? e)).toContain("goto"); } }); // 2. basic scope exit test("close: nil ignored; false is non-closable", async () => { const { e, ref, code } = await runAndCatch( ` do local x = nil local y = false end `, "close_nil_false.lua", ); expect(ref).toContain("close_nil_false.lua"); expect(code).toContain("local y = false"); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: normal close gets nil error", async () => { const env = await evalBlock(` t = {} do local x = setmetatable({"X"}, { __close = function(self, err) if err == nil then table.insert(t, "nil") else table.insert(t, "not-nil") end end }) end `); expect((env.get("t") as any).get(1)).toEqual("nil"); }); test("close: close order", async () => { const env = await evalBlock(` t = {} do local a = setmetatable({1}, { __close = function(self) table.insert(t, self[1]) end }) local b = setmetatable({2}, { __close = function(self) table.insert(t, self[1]) end }) end `); expect((env.get("t") as any).get(1)).toEqual(2); expect((env.get("t") as any).get(2)).toEqual(1); }); test("close: shadowed variables close independently", async () => { const env = await evalBlock(` t = {} do local x = setmetatable({"OUTER"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) do local x = setmetatable({"INNER"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) table.insert(t, "inner-end") end table.insert(t, "middle") end table.insert(t, "outer-end") `); const t = env.get("t") as any; expect(t.get(1)).toEqual("inner-end"); expect(t.get(2)).toEqual("close-INNER"); expect(t.get(3)).toEqual("middle"); expect(t.get(4)).toEqual("close-OUTER"); expect(t.get(5)).toEqual("outer-end"); }); test("close: metatable swap after mark closes with new __close", async () => { const env = await evalBlock(` t = {} do local mt1 = { __close = function() table.insert(t, "close-1") end } local mt2 = { __close = function() table.insert(t, "close-2") end } local x = setmetatable({}, mt1) setmetatable(x, mt2) end `); const t = env.get("t") as any; // Lua uses the metamethod at close time; this verifies close-time // lookup expect(t.get(1)).toEqual("close-2"); }); test("close: replacing __close function affects close-time behavior", async () => { const env = await evalBlock(` t = {} do local mt = { __close = function() table.insert(t, "close-1") end } local x = setmetatable({}, mt) mt.__close = function() table.insert(t, "close-2") end end `); const t = env.get("t") as any; expect(t.length).toEqual(1); expect(t.get(1)).toEqual("close-2"); }); // 3. errors and unwinding test("close: non-closable init", async () => { const { e } = await runAndCatch(` do local x = {} end `); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: reassign after implicit nil errors", async () => { const { e } = await runAndCatch( ` do local a a = 1 end `, "close_reassign_after_implicit_nil.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain( "attempt to assign to const variable 'a'", ); }); test("close: reassign after explicit nil errors", async () => { const { e } = await runAndCatch( ` do local a = nil a = 1 end `, "close_reassign_after_explicit_nil.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain( "attempt to assign to const variable 'a'", ); }); test("close: reassign after false is non-closable", async () => { const { e } = await runAndCatch( ` do local a = false a = 1 end `, "close_reassign_after_false.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: reassign after closable value errors", async () => { const { e } = await runAndCatch( ` do local a = setmetatable({"A"}, { __close = function(self) end }) a = 1 end `, "close_reassign_after_closable.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain( "attempt to assign to const variable 'a'", ); }); test("close: reassignment through closure is non-closable", async () => { const { e } = await runAndCatch( ` do local a = false local function g() a = nil end g() end `, "close_reassign_through_closure.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: initializer true is non-closable", async () => { const { e } = await runAndCatch( ` do local a = true end `, "close_init_true.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: initializer integer is non-closable", async () => { const { e } = await runAndCatch( ` do local a = 1 end `, "close_init_int.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: initializer float is non-closable", async () => { const { e } = await runAndCatch( ` do local a = 3.14 end `, "close_init_float.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: initializer string is non-closable", async () => { const { e } = await runAndCatch( ` do local a = "string" end `, "close_init_string.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: initializer function is non-closable", async () => { const { e } = await runAndCatch( ` do local a = function() end end `, "close_init_function.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: __close called exactly once on normal exit", async () => { const env = await evalBlock(` t = {} do local called = 0 local x = setmetatable({}, { __close = function() called = called + 1 table.insert(t, called) end }) end `); const t = env.get("t") as any; expect(t.length).toEqual(1); expect(t.get(1)).toEqual(1); }); test("close: async __close awaited on normal exit", async () => { const G = luaBuildStandardEnv(); const env = new LuaEnv(G); // Promise-returning function: forces async close path deterministically. (env as any).setLocal( "delay", (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), ); await evalBlock( ` t = {} do local x = setmetatable({}, { __close = function() delay(10) table.insert(t, "closed") end }) end `, env, ); const t = env.get("t") as any; expect(t.length).toEqual(1); expect(t.get(1)).toEqual("closed"); }); test("close: __close called exactly once on error unwind", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} called = 0 do local x = setmetatable({}, { __close = function(self, err) called = called + 1 table.insert(t, called) end }) error("boom") end `, "close_once_on_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); const t = env.get("t") as any; expect(t.length).toEqual(1); expect(t.get(1)).toEqual(1); }); test("close: async __close awaited on error unwind", async () => { const code = ` t = {} do local x = setmetatable({"X"}, { __close = function(self, err) delay(10) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) error("boom") end `; const ast = parse(code, { ref: "close_async_unwind.lua" }); const G = luaBuildStandardEnv(); const env = new LuaEnv(G); const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx); env.setLocal( "delay", (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), ); let e: unknown = null; try { const r = evalStatement(ast, env, sf, false); if (isPromise(r)) { await r; } } catch (err: unknown) { e = err; } expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); const t = env.get("t") as any; expect(t.length).toEqual(1); expect(t.get(1)).toEqual("close-X-boom"); }); test("close: error in __close inside vararg function", async () => { const { e } = await runAndCatch( ` local function mk() return setmetatable({}, { __close = function() error("closefail") end }) end local function f(...) local x = mk() return ... end f(1, 2, 3) `, "close_vararg_close_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("closefail"); }); test("close: __close not callable", async () => { const { e } = await runAndCatch( ` do local x = setmetatable({}, { __close = 1 }) end `, "close_not_callable.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: __close not found via __index", async () => { const { e } = await runAndCatch( ` do local mt = { __index = { __close = function() end } } local x = setmetatable({}, mt) end `, "close_index_metaclose.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: mutate __close", async () => { const { e } = await runAndCatch( ` do local mt = { __close = function() end } local x = setmetatable({}, mt) mt.__close = 1 end `, "close_mutate.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("not callable"); }); test("close: remove __close after mark errors at close time", async () => { const { err: e } = await runAndCatchEnv( ` do local mt = { __close = function() end } local x = setmetatable({}, mt) mt.__close = nil end `, "close_remove_metaclose.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("not callable"); }); test("close: __close callable via __call", async () => { const { e } = await runAndCatch( ` do local c = setmetatable({}, { __call = function(self, obj, err) table.insert(t, "called") end }) local x = setmetatable({}, { __close = c }) end `, "close_callable_via_call.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: error after close var init still closes", async () => { const { e } = await runAndCatch( ` t = {} local function mk() return setmetatable({"A"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) end do local a = mk() error("boom") end `, "close_error_after_init_closes.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); }); test("close: close gets error", async () => { const { e } = await runAndCatch(` t = {} local function mk() return setmetatable({}, { __close = function(self, err) table.insert(t, tostring(err)) end }) end do local a = mk() error("boom") end `); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); }); test("close: close error", async () => { const { e } = await runAndCatch( ` do local x = setmetatable({}, { __close = function() error("closefail") end }) end `, "close_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("closefail"); }); test("close: close error on error", async () => { const { e } = await runAndCatch( ` do local x = setmetatable({}, { __close = function() error("closefail") end }) error("boom") end `, "close_error_on_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("closefail"); }); test("close: close errors stop", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} do local a = setmetatable({"A"}, { __close = function(self) table.insert(t, "close-" .. self[1]) error("c1") end }) local b = setmetatable({"B"}, { __close = function(self) table.insert(t, "close-" .. self[1]) error("c2") end }) end `, "close_errors_stop.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("c2"); // verify that both closes were attempted (Lua 5.4), but the first // close error (from B, closed first) is the reported error const t = env.get("t") as any; expect(t.length).toEqual(2); expect(t.get(1)).toEqual("close-B"); expect(t.get(2)).toEqual("close-A"); }); test("close: multiple closers unwind on error", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function mk(x) return setmetatable({x}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end do local a = mk("A") local b = mk("B") error("boom") end `, "close_multi_unwind.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); // verify both were closed in reverse order with the error object const t = env.get("t") as any; expect(t.length).toEqual(2); expect(t.get(1)).toEqual("close-B-boom"); expect(t.get(2)).toEqual("close-A-boom"); }); test("close: close errors stop during unwind", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function mk(x, fail) return setmetatable({x}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) if fail then error("closefail-" .. self[1]) end end }) end do local a = mk("A", false) local b = mk("B", true) error("boom") end `, "close_unwind_close_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("closefail-B"); // verify both closes were attempted (Lua 5.4), but the first close // error (from B, closed first) is the reported error const t = env.get("t") as any; expect(t.length).toEqual(2); expect(t.get(1)).toEqual("close-B-boom"); expect(t.get(2)).toEqual("close-A-boom"); }); test("close: complex assignment error closes prior", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function f1() return setmetatable({}, { __close = function() table.insert(t, "a_closed") end }) end local function f2() error("assign_error") end do local a = f1() local b = f2() end `, "complex_assign_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("assign_error"); // verify 'a' was closed despite 'b' failing to assign const t = env.get("t") as any; expect(t.get(1)).toEqual("a_closed"); }); test("close: multi-init closes prior when later init errors", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function mk() return setmetatable({}, { __close = function() table.insert(t, "a_closed") end }) end local function fail() error("boom") end do local a, b = mk(), fail() end `, "close_multi_init_later_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); const t = env.get("t") as any; expect(t.get(1)).toEqual("a_closed"); }); test("close: async close error reported; later closers still run", async () => { const G = luaBuildStandardEnv(); const env = new LuaEnv(G); env.setLocal( "delay", (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), ); const { err: e } = await (async () => { const code = ` t = {} do local a = setmetatable({"A"}, { __close = function(self, err) delay(5) table.insert(t, "close-" .. self[1]) end }) local b = setmetatable({"B"}, { __close = function(self, err) delay(5) table.insert(t, "close-" .. self[1]) error("b-closefail") end }) end `; const ast = parse(code, { ref: "close_async_close_error.lua" }); const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx); let err: unknown = null; try { const r = evalStatement(ast, env, sf, false); if (isPromise(r)) await r; } catch (x) { err = x; } return { err }; })(); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("b-closefail"); const t = env.get("t") as any; // B closes first and errors, but A must still be closed in Lua 5.4 intent expect(t.length).toEqual(2); expect(t.get(1)).toEqual("close-B"); expect(t.get(2)).toEqual("close-A"); }); // 4. control flow exits test("close: return closes", async () => { const env = await evalBlock(` t = {} local function f() do local x = setmetatable({"X"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return 1 end end table.insert(t, f()) `); expect((env.get("t") as any).get(1)).toEqual("close-X"); expect((env.get("t") as any).get(2)).toEqual(1); }); test("close: return in if closes", async () => { const env = await evalBlock(` t = {} local function f() do local x = setmetatable({"X"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) if true then return 2 end return 3 end end table.insert(t, f()) `); expect((env.get("t") as any).get(1)).toEqual("close-X"); expect((env.get("t") as any).get(2)).toEqual(2); }); test("close: goto closes", async () => { const env = await evalBlock(` t = {} do local x = setmetatable({"X"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) goto L1 ::L2:: error("bad") ::L1:: goto L3 ::L3:: end `); expect((env.get("t") as any).get(1)).toEqual("close-X"); }); test("close: goto nested", async () => { const env = await evalBlock(` t = {} do do local a = setmetatable({"A"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) do local b = setmetatable({"B"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) goto L end end ::L:: end `); expect((env.get("t") as any).get(1)).toEqual("close-B"); expect((env.get("t") as any).get(2)).toEqual("close-A"); }); test("close: return to-be-closed variable", async () => { const env = await evalBlock(` t = {} local function f() local x = setmetatable({"X"}, { __close = function(self) table.insert(t, "closed") end }) return x end local r = f() table.insert(t, type(r)) `); const t = env.get("t") as any; expect(t.get(1)).toEqual("closed"); expect(t.get(2)).toEqual("table"); }); // 5. generic-for loop-scoped closing test("close: for-in no close", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1, "v" end return nil end local function gen() return iter, nil, nil end for k, v in gen() do table.insert(t, "body") end `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).length).toEqual(1); }); test("close: for-in false close is non-closable", async () => { const { e } = await runAndCatch( ` local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end local function gen() return iter, nil, nil, false end for k in gen() do end `, "for_in_false_close.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: for-in bad close", async () => { const { e } = await runAndCatch( ` local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end local function gen() return iter, nil, nil, {} end for k in gen() do end `, "for_in_bad_close.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("non-closable"); }); test("close: for-in updates control", async () => { const env = await evalBlock(` t = {} local calls = 0 local function iter(state, ctrl) calls = calls + 1 if ctrl == nil then return 1 end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return iter, nil, nil, closing end for k in gen() do table.insert(t, "body") end table.insert(t, calls) `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).get(2)).toEqual("close-C"); expect((env.get("t") as any).get(3)).toEqual(2); }); test("close: for-in closes", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1, "v" end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return iter, nil, nil, closing end for k, v in gen() do table.insert(t, "body") end `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).get(2)).toEqual("close-C"); }); test("close: for-in async iterator and async closing are awaited", async () => { const G = luaBuildStandardEnv(); const env = new LuaEnv(G); (env as any).setLocal( "delay", (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), ); await evalBlock( ` t = {} local calls = 0 local function iter(state, ctrl) calls = calls + 1 delay(5) if ctrl == nil then return 1 end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self, err) delay(5) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return iter, nil, nil, closing end for k in gen() do table.insert(t, "body") end table.insert(t, calls) `, env, ); const t = env.get("t") as any; expect(t.get(1)).toEqual("body"); expect(t.get(2)).toEqual("close-C-nil"); expect(t.get(3)).toEqual(2); }); test("close: loop scope", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return iter, nil, nil, closing end for k in gen() do table.insert(t, "body") end table.insert(t, "after") `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).get(2)).toEqual("close-C"); expect((env.get("t") as any).get(3)).toEqual("after"); }); test("close: for-in closing closes before outer block closers", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end do local outer = setmetatable({"O"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) local function gen() local closing = setmetatable({"L"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return iter, nil, nil, closing end for k in gen() do table.insert(t, "body") end table.insert(t, "after-loop") end table.insert(t, "after-block") `); const t = env.get("t") as any; // Required ordering: // body runs // loop closing value closes at loop end // after-loop runs // outer closes when leaving do-block // after-block runs expect(t.get(1)).toEqual("body"); expect(t.get(2)).toEqual("close-L"); expect(t.get(3)).toEqual("after-loop"); expect(t.get(4)).toEqual("close-O"); expect(t.get(5)).toEqual("after-block"); }); test("close: for-in closes on break", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1, "v" end return 2, "w" end local function gen() local closing = setmetatable({"C"}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) return iter, nil, nil, closing end for k, v in gen() do table.insert(t, "body") break end `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).get(2)).toEqual("close-C"); }); test("close: for-in closes on error", async () => { const { err: e, env } = await runAndCatchEnv(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1, "v" end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return iter, nil, nil, closing end for k, v in gen() do error("boom") end `); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); // verify closer received error const t = env.get("t") as any; expect(t.get(1)).toEqual("close-C-boom"); }); test("close: for-in closes if iterator errors", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function iter(state, ctrl) error("iterboom") end local function gen() local closing = setmetatable({"C"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return iter, nil, nil, closing end for k in gen() do end `, "for_in_iterator_error.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("iterboom"); // verify closer received error const t = env.get("t") as any; expect(t.get(1)).toEqual("close-C-iterboom"); }); test("close: return inside for-in body closes loop closing value", async () => { const env = await evalBlock(` t = {} local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return iter, nil, nil, closing end local function f() for k in gen() do return 1 end return 2 end table.insert(t, f()) `); const t = env.get("t") as any; expect(t.get(1)).toEqual("close-C-nil"); expect(t.get(2)).toEqual(1); }); test("close: error inside for-in body closes loop closing value with error", async () => { const { err: e, env } = await runAndCatchEnv( ` t = {} local function iter(state, ctrl) if ctrl == nil then return 1 end return nil end local function gen() local closing = setmetatable({"C"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return iter, nil, nil, closing end for k in gen() do error("boom") end `, "for_in_body_error_close_value.lua", ); expect(e).toBeInstanceOf(LuaRuntimeError); expect((e as LuaRuntimeError).message).toContain("boom"); const t = env.get("t") as any; expect(t.get(1)).toEqual("close-C-boom"); }); // 6. pairs integration test("close: pairs closes", async () => { const env = await evalBlock(` t = {} local closed = false local a = {10, 20} local function iter(state, ctrl) if ctrl == nil then return 1, state[1] end return nil end setmetatable(a, { __pairs = function(self) local tbc = setmetatable({"P"}, { __close = function() table.insert(t, "close-P") closed = true end }) return iter, self, nil, tbc end }) for k, v in pairs(a) do table.insert(t, "body") end if closed then table.insert(t, "closed") end `); expect((env.get("t") as any).get(1)).toEqual("body"); expect((env.get("t") as any).get(2)).toEqual("close-P"); expect((env.get("t") as any).get(3)).toEqual("closed"); }); // 7. protected calls test("close: pcall closes on success", async () => { const env = await evalBlock(` t = {} local function f() local x = setmetatable({"X"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return 1 end local ok, r = pcall(f) if ok then table.insert(t, r) end `); expect((env.get("t") as any).get(1)).toEqual("close-X-nil"); expect((env.get("t") as any).get(2)).toEqual(1); }); test("close: multi-return expansion binds and closes", async () => { const env = await evalBlock(` t = {} local function mk() local obj = setmetatable({"A"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) return obj, 99 end do local a, b = mk() table.insert(t, b) end `); const t = env.get("t") as any; expect(t.get(1)).toEqual(99); expect(t.get(2)).toEqual("close-A-nil"); }); test("close: pcall return closes with nil error", async () => { const env = await evalBlock(` t = {} local function mk() return setmetatable({"R"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end local function f() do local x = mk() return 7 end end local ok, r = pcall(f) table.insert(t, ok) table.insert(t, r) `); const t = env.get("t") as any; expect(t.get(1)).toEqual("close-R-nil"); expect(t.get(2)).toEqual(true); expect(t.get(3)).toEqual(7); }); test("close: pcall break closes with nil error", async () => { const env = await evalBlock(` t = {} local function mk() return setmetatable({"B"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end local function f() do local x = mk() while true do break end end return 1 end local ok, r = pcall(f) table.insert(t, ok) table.insert(t, r) `); const t = env.get("t") as any; expect(t.get(1)).toEqual("close-B-nil"); expect(t.get(2)).toEqual(true); expect(t.get(3)).toEqual(1); }); test("close: pcall goto closes with nil error", async () => { const env = await evalBlock(` t = {} local function mk() return setmetatable({"G"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end local function f() do local x = mk() goto L error("unreachable") ::L:: end return 2 end local ok, r = pcall(f) table.insert(t, ok) table.insert(t, r) `); const t = env.get("t") as any; expect(t.get(1)).toEqual("close-G-nil"); expect(t.get(2)).toEqual(true); expect(t.get(3)).toEqual(2); }); test("close: pcall closes on error", async () => { const env = await evalBlock(` t = {} local function f() local x = setmetatable({"X"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) error("boom") end local ok, msg = pcall(f) if not ok then table.insert(t, tostring(msg)) end `); expect((env.get("t") as any).get(1)).toEqual("close-X-boom"); expect((env.get("t") as any).get(2)).toEqual("boom"); }); test("close: pcall close error overrides original and skips remaining closers", async () => { const env = await evalBlock(` t = {} local function mk(label, mode) return setmetatable({label}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) if mode == "error" then error("closefail-" .. self[1]) end end }) end local function f() do local a = mk("A", "ok") local b = mk("B", "error") local c = mk("C", "ok") error("boom") end end local ok, msg = pcall(f) table.insert(t, ok) table.insert(t, tostring(msg)) `); const t = env.get("t") as any; // C closes first, then B closes and errors; A is still closed in Lua 5.4 expect(t.get(1)).toEqual("close-C-boom"); expect(t.get(2)).toEqual("close-B-boom"); expect(t.get(3)).toEqual("close-A-boom"); expect(t.get(4)).toEqual(false); expect(t.get(5)).toEqual("closefail-B"); expect(t.length).toEqual(5); }); test("close: xpcall closes on error", async () => { const env = await evalBlock(` t = {} local function f() local x = setmetatable({"X"}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) error("boom") end local function h(err) return "handled-" .. tostring(err) end local ok, msg = xpcall(f, h) if not ok then table.insert(t, tostring(msg)) end `); expect((env.get("t") as any).get(1)).toEqual("close-X-boom"); expect((env.get("t") as any).get(2)).toEqual("handled-boom"); }); test("close: xpcall boundary contains __close errors", async () => { const env = await evalBlock(` t = {} local function f() do local x = setmetatable({}, { __close = function(self, err) table.insert(t, "close-err-" .. tostring(err)) error("closefail") end }) error("boom") end end local function h(err) table.insert(t, "handler-" .. tostring(err)) return "handled-" .. tostring(err) end local ok, msg = xpcall(f, h) table.insert(t, ok) table.insert(t, tostring(msg)) `); const t = env.get("t") as any; // close runs during unwind and sees original error expect(t.get(1)).toEqual("close-err-boom"); // the close error overrides the original for `xpcall`, so the // handler sees "closefail" expect(t.get(2)).toEqual("handler-closefail"); expect(t.get(3)).toEqual(false); expect(t.get(4)).toEqual("handled-closefail"); expect(t.length).toEqual(4); }); test("close: nested to-be-closed created inside __close", async () => { const env = await evalBlock(` t = {} local function mk(label) return setmetatable({label}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end do local outer = setmetatable({"OUTER"}, { __close = function(self, err) table.insert(t, "outer-close-start-" .. tostring(err)) do local inner = mk("INNER") table.insert(t, "inner-scope-end") end table.insert(t, "outer-close-end") end }) table.insert(t, "body-end") end `); const t = env.get("t") as any; // Expected order: // - body finishes // - outer close begins // - inner scope ends, so inner closes (during exec of outer __close) // - outer close ends expect(t.get(1)).toEqual("body-end"); expect(t.get(2)).toEqual("outer-close-start-nil"); expect(t.get(3)).toEqual("inner-scope-end"); expect(t.get(4)).toEqual("close-INNER-nil"); expect(t.get(5)).toEqual("outer-close-end"); }); test("close: pcall boundary does not close closers created in pcall args", async () => { const env = await evalBlock(` t = {} local function mk(label) return setmetatable({label}, { __close = function(self) table.insert(t, "close-" .. self[1]) end }) end local function f() table.insert(t, "in-f") return 1 end local function arg() local x = mk("ARG") table.insert(t, "arg") return 123 end local ok, r = pcall(f, arg()) table.insert(t, ok) table.insert(t, r) `); const t = env.get("t") as any; // if `pcall` boundary is correct, ARG is closed at end of its own // scope and `pcall` does not close it expect(t.get(1)).toEqual("arg"); expect(t.get(2)).toEqual("close-ARG"); expect(t.get(3)).toEqual("in-f"); expect(t.get(4)).toEqual(true); expect(t.get(5)).toEqual(1); }); test("close: nested pcall boundaries", async () => { const env = await evalBlock(` t = {} local function mk(label) return setmetatable({label}, { __close = function(self, err) table.insert(t, "close-" .. self[1] .. "-" .. tostring(err)) end }) end local function inner() local b = mk("B") error("inner") end local function outer() local a = mk("A") local ok, msg = pcall(inner) table.insert(t, ok) table.insert(t, tostring(msg)) return 1 end local ok, r = pcall(outer) table.insert(t, ok) table.insert(t, r) `); const t = env.get("t") as any; // inner closes with "inner" outer continues, then outer closes with // nil on success of outer itself expect(t.get(1)).toEqual("close-B-inner"); expect(t.get(2)).toEqual(false); expect(t.get(3)).toEqual("inner"); expect(t.get(4)).toEqual("close-A-nil"); expect(t.get(5)).toEqual(true); expect(t.get(6)).toEqual(1); });