Programming in Lua - Chapter 16
Compilation, Execution, and Errors
Lua is an interpreted language. Regardless, Lua will always pre-compile source code to a temporary middle-form in the compilation phase. This is standard practice, but feels out of place knowing that interpreted languages are known to be quick and run without any compilers. This chapter is dense and nerdy, but will explain part of the process and why Lua chooses to use that system. This chapter should probably be read on it’s own, as my notes will not be sufficient.
Compilation
Funnily enough, dofile
, a function used just last chapter, is actually an auxiliary function; the work is being done by another function loadfile
.
function dofile (filename)
local f = assert(loadfile(filename))
return f()
end
loadfile
is nice because it does not raise errors, nut returns error codes instead, which is a nicer quality of life difference. dofile
is great for quick and dirty uses, but for more flexibility and customization we can use loadfile
. Part of that flexibility comes from returning not just nil
if it errors, but the code as well. It’s also significantly less costly, since we can call the returned variable from loadfile
throughout a chunk as opposed to running dofile
once per call.
Another expensive call, load
works similarly to loadfile
except it’s mainly used for reading from strings or functions;
f = load("i = i + 1")
i = 0
f(); print(i) --> 1
f(); print(i) --> 2
This should be used with caution. The following line is not only faster but more intuitive to read personally;
f = function () i = i + 1 end
The following example will show another reason for load
to not be your go to option;
i = 32
local i = 0
f = load("i = i + 1; print(i)")
g = function () i = i + 1; print(i) end
f() --> 33
g() --> 1
load
will always access global variables, while functions will grab local. load
is best used for running external code.
Precompiled Code
We already know that Lua precompiles source code. Lua also allows for distributing code in precompiled forms, or binary chunks, is with the luac
program built in. The following creates a prog.lc
files using the precompiled version of prog.lua
;
luac -o prog.lc prog.lua
Precompiled code is not always smaller but always faster to run, and has the benefit of protecting against changes of sources. You should never run untrusted precompiled code, as that can lead to loads of different potential malicious injections.
Errors
Due to the amount of applications that rely on Lua, crashes aren’t desirable. We must be able to handle errors, which Lua does well. Anytime Lua runs into an unexpected condition it will raise an error, or you can directly call an error using the error
function with some message for the error;
print "Give me a number please: "
n = io.read("n")
if not n then error("invalid input") end
Because this happens so often, Lua has a built-in function that can handle this for us, assert
. It checks if the first argument is false, and if it is raises an error with the provided message;
print "Give me a number please: "
n = assert(io.read("n"), "invalid input")
Whenever running into exceptions, Lua has two ways of dealing with them, either returning an error code of nil
or false
, or raising an error. There is no explicit decision between the two, but the general rule is that an exception that is easily avoided should raise an error; otherwise it should return an error code.
Error Handling and Exceptions
Since Lua is used alongside other languages, often we don’t need Lua to do any much handling. If we do want to handle errors, we should use pcall
or a protected call to encapsulate our chunks.
local ok, msg = pcall(function ()
<some code>
if unexpected_condition then error() end
<some code>
print(a[i])
<some code>
end)
if ok then
<regular code>
else
<error-handling code>
end
Now, if there are no errors, pcall
returns true
alongside any values returned by the call. Otherwise, it returns false
with the corresponding error message.
Error Messages and Tracebacks
We can use any value for an error message, not just strings. During an internal error, Lua will generate the default error object, a string. If it’s not a string, it’s the value passed to the function error
.
local status, err = pcall(function () error("my error") end)
print(err) --> stdin:1: my error
Let’s look at how Lua will handle the following;
function foo (str)
if type(str) ~= "string" then
error("string expected")
end
<regular code>
end
If someone tries to make the following incorrect call;
foo({x = 1})
Lua will think the error is foo
’s fault, rather than the person who made this call. We can fix this by appending 2 to the error call (error("string expected", 2)
). This will let Lua know the error happened on level 2 instead of 1 (our function).
A traceback is the collection of calls that lead to an error message. In order to retrieve this, we have to actually make it before the call to pcall
returns it. We can use xpcall
in this scenario, where the second argument is a message handler function designed to call before the stack unwinds, and collects all the relevant information on the error. The two message handlers provided by Lua are debug.debug
and debug.traceback
. The latter is more verbose and is used by the stand-alone interpreter by default.
Exercises
- Write a function,
loadwithprefix
, that works likeload
, except it adds its extra first argument as a prefix to the chunk being loaded. It should callload
that returns the prefix, thenload
for the remaining original chunk.
Next: Chapter 17