Skip to content

Instantly share code, notes, and snippets.

@johnd0e
Last active August 27, 2024 20:49
Show Gist options
  • Save johnd0e/17918a5199aaecb1747f3b2c650322ce to your computer and use it in GitHub Desktop.
Save johnd0e/17918a5199aaecb1747f3b2c650322ce to your computer and use it in GitHub Desktop.
Exception control - lua implementation of ltn13 (from luasocket)
-- idea by Diego Nehab: https://github.com/lunarmodules/luasocket/blob/master/ltn013.md
-- improved by Peter Melnichenko: https://github.com/lunarmodules/luasocket/pull/161/files
-- https://github.com/mpeterv/luasocket/blob/e7b68bb49ce1a40effadda48969b9377e30887cb/src/except.lua
-- https://github.com/mpeterv/luasocket/blob/e7b68bb49ce1a40effadda48969b9377e30887cb/test/excepttest.lua
-- With some changes from John Doe:
-- - finalizer's call is unprotected, like in luasocket
-- - `except.try`, like in luasocket
-- - `except.newtry` demands a finalizer
-- - argument for finalizer can be specified
-- - on error `except.protect` returns the exact return values (instead of just `nil, arg[2]`)
-- - new `except.call` is same as `pcall` but does not return first `success` value if it is `true`.
-----------------------------------------------------------------------------
-- Exception control
-- LuaSocket toolkit (but completely independent from other modules)
-- Author: Diego Nehab
-- This provides support for simple exceptions in Lua. During the
-- development of the HTTP/FTP/SMTP support, it became aparent that
-- error checking was taking a substantial amount of the coding. These
-- function greatly simplify the task of checking errors.
-- The main idea is that functions should return nil as its first return
-- value when it finds an error, and return an error message (or value)
-- following nil. In case of success, as long as the first value is not nil,
-- the other values don't matter.
-- The idea is to nest function calls with the "try" function. This function
-- checks the first value, and, if it's falsy, wraps the second value
-- in a table with metatable and calls "error" on it. Otherwise,
-- it returns all values it received.
-- The "newtry" function is a factory for "try" functions that call a finalizer
-- in protected mode before calling "error".
-- The "protect" function returns a new function that behaves exactly like the
-- function it receives, but the new function catches exceptions
-- thrown by "try" functions and returns nil followed by the error message
-- instead.
-- With these three function, it's easy to write functions that throw
-- exceptions on error, but that don't interrupt the user script.
-----------------------------------------------------------------------------
local base = _G
local _M = {}
local exception_metat = {}
function exception_metat:__tostring()
return base.tostring(self[1])
end
local function do_nothing() end
function _M.newtry(finalizer, arg)
assert(type(finalizer)=="function", "finalizer function expected")
return function(ok, ...)
if ok then
return ok, ...
else
finalizer(arg)
base.error(base.setmetatable({
n=select("#", ...), [0]=ok, ...
}, exception_metat))
end
end
end
local function handle_pcall_returns(ok, ...)
if ok then
return ...
else
local err = ...
if base.getmetatable(err) == exception_metat then
return unpack(err, 0, err.n)
else
base.error(err, 0)
end
end
end
function _M.protect(func)
return function(...)
return handle_pcall_returns(base.pcall(func, ...))
end
end
_M.try = _M.newtry(do_nothing)
local function shift (...)
if ... then
return base.select(2,...)
end
return ...
end
function _M.call (...)
return shift(base.pcall(...))
end
if ...=="test" then
return _M
end
-------------------------------------
local except = _M -- require "except"
local finalizer_called
local func = except.protect(function(err, ...)
local try = except.newtry(function()
finalizer_called = true
end)
if err then
return error(err, 0)
else
return try(...)
end
end)
local ret1, ret2, ret3 = func(false, 1, 2, 3)
assert(not finalizer_called, "unexpected finalizer call")
assert(ret1 == 1 and ret2 == 2 and ret3 == 3, "incorrect return values")
ret1, ret2, ret3 = func(false, false, "error message")
assert(finalizer_called, "finalizer not called")
assert(ret1 == false and ret2 == "error message" and ret3 == nil, "incorrect return values")
local err = {key = "value"}
ret1, ret2 = pcall(func, err)
assert(not ret1, "error not rethrown")
assert(ret2 == err, "incorrect error rethrown")
-- JD
assert(pcall(except.try,1,2,3), "try: should not throw on true value")
ret1, ret2, ret3 = except.try(1,2,3)
assert(ret1 == 1 and ret2 == 2 and ret3 == 3, "try: incorrect return values")
func = except.protect(function(...)
except.try(...)
return "success"
end)
ret1, ret2, ret3 = func(false, 2, 3)
assert(ret1 == false and ret2 == 2 and ret3 == 3, "incorrect return values")
func = except.protect(function(finalizer, ...)
local try = except.newtry(finalizer, "finalizer_arg")
try(...)
return "success"
end)
assert(not pcall(func, error, nil), "protect: should propagate error in finalizer")
local finalizer_arg
func(function (arg) finalizer_arg = arg end, nil)
assert(finalizer_arg=="finalizer_arg", "argument should be passed to finalizer")
local success, errmsg = pcall(func)
assert(not success and errmsg:match("finalizer function expected$"), "newtry: finalizer must be specified")
ret1, ret2, ret3 = func(do_nothing, nil,2,3)
assert(ret1==nil and ret2==2 and ret3==3, "protect: on error all the values must be returned as they are")
local ret1p, ret2p, ret3p, _
ret1p, ret2p = pcall(error, "Error")
ret1, ret2 =except.call(error, "Error")
assert(ret1==ret1p and ret2==ret2p, "call: on error behaves exactly like pcall")
_, ret2p, ret3p = pcall(function() return 1,2 end)
ret1, ret2 =except.call(function() return 1,2 end)
assert(ret1==ret2p and ret2==ret3p, "call: on success returns only func return value")
print("OK")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment