8.1. Lua

Lua is a general purpose scripting language. It is well-known for having a small and fast implementation but a wide set of features including co-operative threading (co-routines), closures, "self"-style object orientation, and so on. Lua is more expressive and faster than all of the mainstream scripting languages and is used in many computer games.

To learn Lua quickly, read chapter 2 of its manual.

Lua is used in Grit with some minor modifications that make Lua code written for Grit incompatible with a regular Lua interpreter and regular Lua programs potentially incompatible with Grit. This is done for performance and usability.

It should be possible to make regular Lua code work in Grit with close to zero modifications and the same performance. It is very unlikely that Grit Lua code will work outside of the customized Grit Lua VM without running much more slowly.

This section details the Lua modifications. You can skim it if you are not already very familiar with Lua, and if you're not intending to port Lua code from external sources to run in Grit.

8.1.1. Relative Path Literals

It is convenient for Lua files to refer to resources via relative paths. This makes it possible to define packages of scripts and resources, that can be downloaded and unpacked anywhere in the directory hierarchy without having to edit the Lua files to correct absolute paths.

However, when using strings to represent relative paths, the strings can be passed by value through function calls and variable assignments and end up being used in a different Lua file, where the relative path no-longer resolved correctly.

In order for the relative paths to be resolved in the file where they are defined, Lua has been extended to add another kind of string literal:

print(`foo`)
print(`/dir/foo`)
print(`../foo`)
print(`.`)

This string is assumed to be a path. If the path begins with a / then it behaves the same as an ordinary string. However, other paths are assumed to be relative, and will be resolved using the directory of the currently-executing Lua file. If such a dir cannot be found, / will be used. cannot be found).

The path will also be canonicalised, i.e., use of . and .. will be removed. An error is raised if too many .. descend below the root of the game directory.

assert(`foo/../bar` == `bar`)
assert(`foo/../bar/.` == `bar`)
assert(`./foo/../bar` == `bar`)
assert(`/.` == `/`)

8.1.2. Unicode

All Lua files are now UTF-8. The string class in Lua has been rewritten to properly understand unicode. E.g. #"£" is 1, not 2. The most visible difference is that the syntax for regular expressions has changed. The new syntax is that of ICU's regular expression engine. For documentation, see sections "Regular Expression Metacharacters", "Regular Expression Operators" and "Replacement Text".

8.1.3. Vectors & Quaternions

Grit Lua scripts are often used to transform and manipulate objects in screen space and 3D space, as well as to manipulate colours. This is easiest with primitive vector and quaternion values. For performance reasons, we cannot use userdata or tables for our vector and quaternion values. This puts too much pressure on the garbage collector. Thus, the VM has been modified so that vector2, vector3, vector4, and quat are primitive types just like 'number', 'boolean' and 'nil'. They are copied by value and are not garbage collected. The following code shows some of the ways they can be used:

Q_ID = quat(1, 0, 0, 0)      -- constructing quaternions and 3d vectors
V_ID = vec(0, 0, 0)
V_NORTH = vec(0, 1, 0)
V_EAST  = vec(1, 0, 0)

local v = vec(1, 2, 3)
print(v)
print(v + v)
print(2 * v)
print(v * 2)
print(v / 2)
print(v * v)                   -- pointwise multiplication
print(dot(v, v))               -- dot product
print(norm(v))                 -- normalise
print(cross(V_NORTH, V_EAST))  -- cross product
print(-v)                      -- opposite direction, same length

print(Q_ID)

print(Q_ID * v2)               -- transform a vector

local q2 = quat(180, vector3(0, 0, 1))  -- angle/axis form
print(q2)
print(q2 * v2)  -- transform a vector by a quaternion

local q3 = Q_ID * q2 -- concatenate quaternion transformations
print(q3)
print(q3 * v2)
print(inv(q3) * q3 * v2)  -- invert a quaternion

print(v2 == v2)  -- equality is pointwise on the elements
print(q2 == q2)
print(q2 ~= q3)

print(#v2) -- length (pythagoras)
print(#q2) -- length (should be 1 for quaternions used to represent rotations)

print(unpack(v2)) -- explode into x, y, z
print(unpack(q2)) -- explode into w, x, y, z

print(quat(90, vector3(0,0,1)) * (V_NORTH + V_EAST)) -- angle/axis constructor form
print(quat(V_NORTH, V_EAST))                         -- quat between two direction vectors
print(quat(V_NORTH, V_EAST) * V_NORTH)

print(V_NORTH.yx) -- arbitrary swizzling is supported

8.1.4. Not A Number (NaN)

If you divide by zero in regular lua, you get a NaN value that goes on to pollute other values, until it eventually causes a problem in an unrelated area of code. This makes debugging difficult because it is hard to find the original cause of the NaN. In Grit we instead trap the divide by zero with an error instead of returning NaN.

8.1.5. Tail Call Optimisations

If the last statement of a Lua function is a call to another function, known as a tail call, the standard Lua compiler will perform an optimisation that results in a small performance improvement and allows unbounded tail call recursion (essentially allowing more programs to be written). Simply put, the interpreter re-uses the existing stack frame for the new call, which avoids using too many stack frames and therefore using too much memory. Here is an example of such a program:

function sum_1_to_n(x, sum)
    if x == 0 then return sum end
    sum = sum or 0
    return sum_1_to_n(x-1, sum+x)
end

Note that it is only a single class of recursive functions that can benefit from tail call optimisation. The following code implements the same function, using regular recursion. This code will create too many stack frames if given a high x input, as the tail call optimisation cannot be applied.

function sum_1_to_n(x)
    if x == 0 then return 0 end
    return x + sum_1_to_n(x-1)
end

Tail call optimisation removes stack frames, and thus removes lines from the stack trace if an error is generated. This can be highly confusing when debugging programs, as you can't find the line number where the bad call occurred.

It is not only in the case of these "tail recursive" functions where the loss of stack frame occurs, but in any function with a tail call. In almost all such cases the optimization does not provide any benefit at all.

We consider the small overall benefit of this feature to be a poor tradeoff for the productivity loss caused its effect on the readability of stack traces. Consequently, it is disabled in Grit. This means programs like the above must be rewritten as follows, to avoid using too many stack frames:

function sum_1_to_n(x)
    local counter
    for i=1, x do
        counter = counter + i
    end
    return counter
end

For an example of where lost debugging information causes a problem, see the following code, where the stack trace does not mention the critical filename or line number where the function was wrongly called. Imagine that there are many calls to do_something_tricky all over the program, and the arguments to the call are computed in each case. This means the stack trace is extremely useful for finding out exactly what went wrong, but the crucial stack frame is lost.

function do_something_tricky(x, y)
    if x ~= y % 3 then
        error "You fool, you called do_something_tricky wrongly."
    end
end

function i_am_buried_somewhere_in_a_million_lines_of_code ()
    x = 42
    y = 666
    return do_something_tricky(x, y) -- problem is here
end