Ian
02-09-2007, 05:44 AM
While I will argue that type safety is simply unnecessary in a dynamically typed language like Lua, the fact that Lua does not generate an error when accessing a variable that doesn't exist is horribly unsafe. This means that any typographical or logic error involving variables can cause confusing and hard to fix bugs. The error won't propagate until the nil value is used in a way that Lua defines as erroneous which doesn't tell you anything about how the variable became nil to begin with. To combat this I've tried to write some functions that do generate errors if you do something unintentional. :mad:
This first function wraps a table and prevents you from trying to read from a non-existent index. Lua would do this by default were it sane. To use it just wrap your table like so: SafeTable({1, 2, 3}) or alternatively SafeTable{1, 2, 3}. SafeTable has side-effects--that is it alters the table in place. I could change this behavior by adding a copy function, but I didn't think it worth it.
-- SafeTable raises an error if a non-existing index is accessed. Note that
-- indices can still be added and removed like usual. Also, while SafeTable
-- returns the table it was passed it also alters it in place.
function SafeTable(tbl)
return setmetatable(tbl, {
-- Define the __index metamethod to change the behavior of indexing.
__index = function (tbl, key)
-- If normal indexing is used then this metamethod will be called
-- causing infinite recursion; rawget ignores the metamethod.
result = rawget(tbl, key)
-- If the value for the given index/key is nil then generate an
-- error. Note that tostring is called because string
-- concatenation is arbitrarily NOT polymorphic and will generate
-- an error if the key is not a string or number.
assert(result ~= nil, "Invalid index: " .. tostring(key) .. ".")
return result
end
})
end
This next function wraps a function so that errors will be raised if it is called with too many or too few arguments, or if it is passed a nil value. To use it, wrap the function declaration with it or decorate it like so: f = SafeFunction(f). The optional second argument tells the function how many arguments to expect; there is sadly no way to glean this information via introspection.
-- SafeFunction raises an error if a nil argument is passed to it that precedes
-- a non-nil argument. Furthermore, if numberOfArguments is defined then it
-- will require that number of arguments be passed to it.
function SafeFunction(func, numberOfArguments)
-- Return the wrapper for func.
return function (...)
-- Create a table from the arguments passed to the wrapper.
local arguments = {...}
local totalArguments = numberOfArguments or #arguments
if #arguments ~= totalArguments then
-- The function was passed more arguments then it takes.
print(string.format("Function takes %d arguments; %d %s passed.",
numberOfArguments, #arguments,
#arguments == 1 and "was" or "were"))
end
for index = 1, totalArguments do
if arguments[index] == nil then
if index > #arguments then
-- The function was not passed enough arguments or the last
-- arguments were nil.
error(string.format(
"Function takes %d arguments; %d %s received.",
numberOfArguments, index - 1,
index - 1 == 1 and "was" or "were"))
else
-- An argument was passed that was nil. This makes the
-- assumption that a symbol was never declared somewhere.
error("A non-existent symbol was passed to parameter " ..
index .. ".")
end
end
end
func(...)
end
end
This last function tests to see if some local variables are nil or if upvalues are nil (these are used in closures.) Note that this is probably the least useful of the three. To use it you can stick it at the end of your function (before the return statement.) For some reason it doesn't work half the time (d'oh!) Alternatively (as a last resort!) you can hook it to be used on every function call using the hook functions provided. This may cause a lot of erroneous errors to be generated. I might add a way to selectively disable them if someone actually finds this useful. (I was too lazy to document this.)
function SafetyTest(event)
-- Test local variables.
local localIndex = 1
while true do
local name, value = debug.getlocal(2, localIndex)
if not name then
break
end
assert(value ~= nil or name == "arg" or name == "(*temporary)", name)
localIndex = localIndex + 1
end
-- Test upvalues.
local func = debug.getinfo(2).func
local upValueIndex = 1
while true do
local name, value = debug.getupvalue(func, upValueIndex)
if not name then
break
end
assert(value ~= nil, name)
upValueIndex = upValueIndex + 1
end
end
function EnableSafetyTest()
debug.sethook(SafeTest, "c")
end
function DisableSafetyTest()
debug.sethook()
end
Phew.
P.S.
Seth, it's not too late to switch to Python (maybe). :(
This first function wraps a table and prevents you from trying to read from a non-existent index. Lua would do this by default were it sane. To use it just wrap your table like so: SafeTable({1, 2, 3}) or alternatively SafeTable{1, 2, 3}. SafeTable has side-effects--that is it alters the table in place. I could change this behavior by adding a copy function, but I didn't think it worth it.
-- SafeTable raises an error if a non-existing index is accessed. Note that
-- indices can still be added and removed like usual. Also, while SafeTable
-- returns the table it was passed it also alters it in place.
function SafeTable(tbl)
return setmetatable(tbl, {
-- Define the __index metamethod to change the behavior of indexing.
__index = function (tbl, key)
-- If normal indexing is used then this metamethod will be called
-- causing infinite recursion; rawget ignores the metamethod.
result = rawget(tbl, key)
-- If the value for the given index/key is nil then generate an
-- error. Note that tostring is called because string
-- concatenation is arbitrarily NOT polymorphic and will generate
-- an error if the key is not a string or number.
assert(result ~= nil, "Invalid index: " .. tostring(key) .. ".")
return result
end
})
end
This next function wraps a function so that errors will be raised if it is called with too many or too few arguments, or if it is passed a nil value. To use it, wrap the function declaration with it or decorate it like so: f = SafeFunction(f). The optional second argument tells the function how many arguments to expect; there is sadly no way to glean this information via introspection.
-- SafeFunction raises an error if a nil argument is passed to it that precedes
-- a non-nil argument. Furthermore, if numberOfArguments is defined then it
-- will require that number of arguments be passed to it.
function SafeFunction(func, numberOfArguments)
-- Return the wrapper for func.
return function (...)
-- Create a table from the arguments passed to the wrapper.
local arguments = {...}
local totalArguments = numberOfArguments or #arguments
if #arguments ~= totalArguments then
-- The function was passed more arguments then it takes.
print(string.format("Function takes %d arguments; %d %s passed.",
numberOfArguments, #arguments,
#arguments == 1 and "was" or "were"))
end
for index = 1, totalArguments do
if arguments[index] == nil then
if index > #arguments then
-- The function was not passed enough arguments or the last
-- arguments were nil.
error(string.format(
"Function takes %d arguments; %d %s received.",
numberOfArguments, index - 1,
index - 1 == 1 and "was" or "were"))
else
-- An argument was passed that was nil. This makes the
-- assumption that a symbol was never declared somewhere.
error("A non-existent symbol was passed to parameter " ..
index .. ".")
end
end
end
func(...)
end
end
This last function tests to see if some local variables are nil or if upvalues are nil (these are used in closures.) Note that this is probably the least useful of the three. To use it you can stick it at the end of your function (before the return statement.) For some reason it doesn't work half the time (d'oh!) Alternatively (as a last resort!) you can hook it to be used on every function call using the hook functions provided. This may cause a lot of erroneous errors to be generated. I might add a way to selectively disable them if someone actually finds this useful. (I was too lazy to document this.)
function SafetyTest(event)
-- Test local variables.
local localIndex = 1
while true do
local name, value = debug.getlocal(2, localIndex)
if not name then
break
end
assert(value ~= nil or name == "arg" or name == "(*temporary)", name)
localIndex = localIndex + 1
end
-- Test upvalues.
local func = debug.getinfo(2).func
local upValueIndex = 1
while true do
local name, value = debug.getupvalue(func, upValueIndex)
if not name then
break
end
assert(value ~= nil, name)
upValueIndex = upValueIndex + 1
end
end
function EnableSafetyTest()
debug.sethook(SafeTest, "c")
end
function DisableSafetyTest()
debug.sethook()
end
Phew.
P.S.
Seth, it's not too late to switch to Python (maybe). :(