1.1. Compiling to a single executable

The following are archived, and/or obviously/explicitly unmaintained:

Tip
Misc Neovim map: nnoremap d<Return> :vsplit term://lua -i %:p<Return> or nmap d<Return> :<C-u>wa \| split \| startinsert \| terminal busted -v --lua luajit tests<Return>

2. Paths configuration

Lua knows about this two environment variables:

LUA_PATH

Paths to search for Lua modules.

LUA_CPATH

Paths to search for C DLLs for modules.

A ;; in the environment variable expands to the default paths.

By default, at least in some versions of Luarocks, it favors just instaling in the "local" directory, which is the one that is not system-wide, and that requires no root permissions. To install something in a non-default directory (like ~/local/rocks), one needs to pass --tree ~/local/rocks everytime on installation. To have a specific directory for every project, like NPM does with node_modules, passing a different directory to --tree each time is fine, but then programs require a helper and a command line argument, as it’s explained in the above article, and which is quoted here for reference:

# Install a package to the local directory "lua_modules"
$ luarocks install --tree lua_modules lpeg
# Create the helper script to load paths
$ cat << EOF > set_paths.lua
local version = _VERSION:match("%d+%.%d+")
package.path = 'lua_modules/share/lua/' .. version .. '/?.lua;lua_modules/share/lua/' .. version .. '/?/init.lua;' .. package.path
package.cpath = 'lua_modules/lib/lua/' .. version .. '/?.so;' .. package.cpath
EOF
# Execute your Lua script using the libraries installed in this directory
$ lua -l set_paths my_script.lua
# Running tests with busted:
$ busted --helper=set_paths

Instead of using the above helper script, environment variables can be used.

3. REPL

The default Lua interpreter can be used interactively when calling it with no arguments, or loading first a Lua file, then dropping to interactive mode. It can inspect values prefixing things with an equals sign:

$ cat preface.lua
inspect = require "inspect"
answer = 42
$ lua -i preface.lua
Lua 5.2.4  Copyright (C) 1994-2015 Lua.org, PUC-Rio
> =answer
42
> answer = {result = 42}
> print(inspect(answer))
{
  result = 42
}
> =answer
table: 0x5616a46f8460

For a more ergonomic REPL, install luarepl and linenoise packages:

luarocks --local install luarepl
luarocks --local install linenoise

Note that linenoise compiled a bit of C code, so it required lua5.2-dev (or whatever default Lua version it picked).

Execute the REPL directly (it’s a shell script wrapper): rep.lua

Important
The way that Lua REPLs work, is that each piece of input is a Lua chunk. That makes each local variable only local for that chunk, so they go away immediately. Use globals instead.

4. Basic building blocks

4.1. Basic functions

  • print() (doesn’t pretty print content, use inspect, see blow)

  • assert() (sinergizes with other functions, see below)

  • type()

  • math.random(min, max) or math.random(max)

4.2. Basic loop

It starts at whatever you want, but note that both the initial and the last value are included, so:

for index = 1, 0 do print("none") end
for index = 1, 1 do print("once") end
for index = 0, 1 do print("twice") end

4.3. Early return

A return can only be at the end of a block, so returning earlier can only be done with the do return end idiom.

local function early(value)
    print "Normal execution"
    do return end                     -- Skips the rest of the function
    print "Now this will be skipped"
end

4.4. Asserting

The assert function:

  • takes the assertion as first argument

  • takes the message to print as second parameter

  • returns the first parameter if asserted, so it can be used in the "happy path" of the execution.

This sinergizes well with standard library functions, like io.open, which returns first the opened file, then an error message if needed.

local file = assert(io.open(name, "r+b"))
-- Will print something like the following if passed a wrong file name:
-- lua: foo.lua:42: non-existing-file.txt: No such file or directory

4.5. Debug output

Use the inspect module:

inspect = require "inspect"
print(inspect(variable))
print(inspect(variable, options))

Or use the REPL to print directly without print(). But in regular Lua, the print is needed.

Note
The type of an object or literal can be found with type(foo).

The inspect library accepts options after the object to inspect, and this is a common one that is useful when using "classes" with middleclass, as it filters to only show data:

local function inspect_data(root)
    return inspect(root, {
        process = function(item, path)
            if  path[#path] ~= inspect.METATABLE and
                path[#path] ~= 'class' and
                type(item)  ~= 'function'
            then
                return item
            end
        end
    })
end

4.6. Tables as arrays, hashes or mixed

Understanding tables is essential, as it’s the data structure that it’s useful for everything. While it is used for array-like behavior, at it’s core is a hash table that serves all the purposes.

  • Keys and values can be anything except nil.

  • Strings as keys are nicer in that are easier to use in literals and support the "dot notation" that makes them look like "objects".

  • Numbers, tables, true, false, or strings which would be forbidden as identifiers, can still be used as keys in literals, but need to be wrapped in [].

> hash = { key='value', [3.14]='pi', ['spaces and/or symbols']=42, [42]='or numbers' }
> inspect(hash)
{
  [3.14] = "pi",
  [42] = "or numbers",
  key = "value",
  ["spaces and/or symbols"] = 42
}

> hash = { [true]='value', [false]=false, [{1, 2}]='table as key'}
> inspect(hash)
{
  [false] = false,
  [true] = "value",
  [{ 1, 2 }] = "table as key"
}

If one value were set to nil, it would have been ignored/removed.

An array is just a table in which the keys are not specified, and which by default are assigned sequentially to numbers, starting from 1.

> array = {1, 2, 3}
> inspect(array)
{ 1, 2, 3 }
> array = {[1]=1, [2]=2, [3]=3}   -- Equivalent
> inspect(array)
{ 1, 2, 3 }

Watch out when mixing numbers as keys with strings, as they might look as if the intention was for it to be an array. Functions like pairs or ipairs just treat the table as one kind or the other. ipairs returns index and value, and only loops over the sequential indexes (so it can’t be used with a sparse array that has indexes 1,2 and 100…​ unless you want to stop at 2). pairs returns key and value, and loops over all the keys, but the order is unspecified.

Trait pairs ipairs

Mainly used for

Objects, hashes

Arrays

Key-value pairs iterated on

All

1 to N, consecutive only

Order

Unspecified

Array order

4.7. Helper for finding in a table

object = { "one", "two", 3,
  [422] = "quux",
  foo = "bar",
}
-- This also could be added to the global `table` table, but in the object, it
-- becomes nicer to call: `object:find(value)` vs `table.find(object, value)`.
object['find'] = function(self, value)
    for k, v in pairs(self) do
        if v == value then
            return k
        end
    end
end
print(object:find('two')) --> 2
print(object:find('bar')) --> 'foo'
Warning
Assumes there every key/value pair is unique (i.e. no same value to different keys, otherwise it would return the first found, which is non-deterministic, but could be fine in some cases still).

4.9. Misc

  • Command line arguments are in the args global array.

  • Finishing from a main script can be done just with return.

  • table.insert(array, value) will append to an array (it can accept a position for when needing to insert in a specific place).

  • The # operator yields the size of an object: #array or #table.array.

5. Truth values

Only nil and false are "falsy", so are the only possible values which will make an if or while not execute their block. Anything else, including 0 or an empty string, table, etc, are all "truthy".

6. Logical operators

The not operator always returns true or false. That is, not not is like converting to boolean the value passed:

not nil     -- true
not not nil -- false
not "x"     -- false
not not "x" -- true
not { }     -- false
not not { } -- true

The and operator ("conjunction operator") returns immediately the first argument if it’s falsy (short-circuit), or otherwise returns the second one, but it never does conversion to boolean:

false and "truthy" -- false
nil   and "truthy" -- nil, but not converted to boolean
true  and 1        -- 1 (not converted)
"ok"  and "truthy" -- "truthy" (not converted)

The or operator ("disjunction operator") also short-circuits, so it returns the first argument if it’s truthy, or evaluates and returns the second otherwise.

> function null() print "null" end
> function one() print "one"; return 1 end
> function two() print "two"; return 2 end
> one() or two()
one
1
> null() or one() or two()
null
one
1

In the above example, null() gets called, and since it’s false, keeps evaluating, reaching one(), which short-circuits by returning truthy, so it never evaluates two().

6.1. Lack of ternary operator

There is no ternary operator, but it can be somewhat simulated in some cases:

value = math.random(2) == 1 and "heads" or "tails"

This works because the and operator has precedence over or, and both operators short circuit. If math.random(2) returns false, it doesn’t need to evaluate "heads" to know that the expression is false, so it proceeds to evaluate "tails", and produces that value.

Note that it is not a real replacement, as it will give a wrong result when the condition is true, then the first value happens to be evaluated and being false:

choice = "condition" and someNilValue or "wrong"
assert(choice=="wrong") -- not what a ternary operator would do!

This assigns "wrong" to choice, because while the "condition" string is true (all strings are truthy), the someNilValue variable gets evaluated to falsy, so the wrong value gets chosen.

This is a problem when you want to pick between two values (e.g. a table of options to a function, where choices from a default table are picked if a certain option is not passed), but the defaults can contain false values. There is no way to avoid an if:

local result={}
for key, value in pairs(defaults) do
    if passed[key] ~= nil then
        result[key] = passed[key]
    else
        result[key] = value
    end
end

Conclusion: don’t use the idiom if the second value is or can be falsy.

Note
Other languages support local sign = if x < 0 then "negative" else "non-negative" end, but that is not possible in Lua.

See more at the page about the ternary operator on the Lua users wiki.

7. Scope details and idioms

Global variables are accessed via _G as well. Then _G._G also refers to the global state:

> _G
table: 0x55f25f32d870
> _G._G
table: 0x55f25f32d870
> _G._G._G
table: 0x55f25f32d870

A scope can be produced without a function, just use do chunk end:

do
  local a2 = 2*a
  local d = sqrt(b^2 - 4*a*c)
  x1 = (-b + d)/a2
  x2 = (-b - d)/a2
end          -- scope of `a2' and `d' ends here
print(x1, x2)

Global variables can be disabled in some ways, including existing libraries. Examples:

8. Function syntax sugar

It is common to see this pattern in LÖVE code to override behavior:

function love.load()
    -- ...
end

This just assigns a function inside an existing table (the function could also exist before: it gets overwritten). One cannot create the table on the fly, needs to exist before, so the love table is provided by the framework.

inspect = require "inspect"
-- Create an empty table. If this is missing, it will error out on the next
-- line: "attempt to index global 'foo' (a nil value)"
foo = {}
-- Create a function `bar` directly inside `foo`.
function foo.bar(args)
    print(inspect(args))
end
-- Exactly the same as the above, but requires a tiny bit more typing.
foo.bar = function(args)
    print(inspect(args))
end
-- Use it as any standalone function:
foo.bar()   -- nil
foo.bar(42) -- 42

The colon notation just allows to pass an extra argument when calling the function: the object before the colon. With the above function, this is what it produces:

> foo:bar()
{
  bar = <function 1>
}
> foo:bar(42)
{
  bar = <function 1>
}

The function only inspects the first argument, and in both cases is the same: the very foo table where the bar function is stored.

The colon notation can also be used to define functions. The difference is that when the function is defined with a colon, it gets an automatic first parameter called self.

This is the whole list of equivalences, as explained in the reference manual:

The statement

function f () body end

translates to

f = function () body end

The statement

function t.a.b.c.f () body end

translates to

t.a.b.c.f = function () body end

The statement

local function f () body end

translates to

local f; f = function () body end

not to

local f = function () body end

(This only makes a difference when the body of the function contains references to f.)

(…​)

The colon syntax is used for defining methods, that is, functions that have an implicit extra parameter self. Thus, the statement

function t.a.b.c:f (params) body end

is syntactic sugar for

t.a.b.c.f = function (self, params) body end

9. More syntax notes for making a DSL

Some additional notes on syntactic features, learned from reading Writing a DSL in Lua.

When calling a function with just one argument which is either a string or a table literal, the parenthesis are optional:

print "hello world"            --> print("hello world")
inspect { foo, bar }           --> inspect({ foo, bar })
-- Also works without whitespace after the function name:
require"module"
inspect{ "42", 42 }
-- Use with another function:
print( inspect {42, "42"} )

It even has high precedence:

> print(tonumber "42" + 1)
43

9.1. Chaining

An call without parenthesis can be chained to another call without parenthesis, but the trick is that the returned value has to be callable (that is, a function, or a table with a __call metamethod).

function outer(p1)
    print("outer invoked", p1)
    return function(p2)
        print("inner invoked", table.unpack(p2))
    end
end
outer "yay" { "it", "works" }
--> outer invoked   yay
--> inner invoked   it      works

10. Simulate debug/release, or different behavior without changing code

How to simulate different runtime behavior at "debug time", that is, during development, instead of in release or production?

One way to inject some different implementation of a function (e.g. warning message vs "no op"), or to set some global variable, is to do it on a module that is not loaded by default, but that can be enabled on the command line as an argument. Seems a bit cleaner (and likely faster) than using an environment variable, which tends to be expensive.

For example:

$ cat example.lua
if enable_debug_globally then
    print('Warning: bad thing happened')
end
$ cat mymodule.lua
print('Debugging enabled')
enable_debug_globally = true
$ lua example.lua
$ lua example.lua -l mymodule
$ lua -l mymodule example.lua
Debugging enabled
Warning: bad thing happened

As shown, the order is important. The library doesn’t get loaded on time to influence the execution of the script if the -l mymodule gets passed after the script.

This can also be achieved injecting a piece of script inline:

$ lua example.lua
$ lua -e 'enable_debug_globally=true' example.lua
Warning: bad thing happened

A better way, of course, is to have a function that prints nothing by default, but which gets overwritten by one that actually prints or asserts on "debug mode".

11. Pass by reference/identity or by value

Tables, functions, threads, and (full) userdata values are objects: variables do not actually contain these values, only references to them. Assignment, parameter passing, and function returns always manipulate references to such values; these operations do not imply any kind of copy.
— Lua 5.1 Reference Manual

Variables of primitive types (string, number, boolean…​) get passed by value.

The following function tries to mutate both arguments, but given that one is a value, and other is a reference (because they are a number and a table respectively), only can change the latter.

local rgb = {r=100, g=42, b=111}  -- reference, because `type(rgb)` => table
local alpha = 0.5                 -- value, because `type(alpha)` => number

function make_opaque(color, transparency)
    color.r = 0       -- Works, modifies the passed table
    color.g = 0       -- Works, modifies the passed table
    color.b = 0       -- Works, modifies the passed table
    transparency = 0  -- WRONG! Only affects the local scope!
    print("Transparency in the local scope now is", transparency)
end

inspect = require "inspect"

print("before:", inspect(rgb))
print("before:", alpha)
make_opaque(rgb, alpha)
print("after: ", inspect(rgb))
print("after: ", alpha)         --> 0.5. Probably not what was intended.

Making copies of variables that are passed by reference, actually just makes a reference:

> a = {}
> print(a)
table: 0x558dd8454510
> b = a
> print(b)
table: 0x558dd8454510
> b.example = 42
> print(a.example)
42

Reassigning to a table with new values, calls the "table constructor", hence making a new table altogether.

> orig = {"one", "two"}
> print(orig)
table: 0x55ce75805940
> copy = orig
> print(copy)
table: 0x55ce75805940
> for i, v in ipairs(copy) do print(i, v) end
1       one
2       two
> orig = {1, 2}   -- changes original by setting a new value, copy unaffected!
> for i, v in ipairs(copy) do print(i, v) end
1       one
2       two
> print(orig)
table: 0x55ce75805a10
> print(copy)
table: 0x55ce75805940

The following example shows how an "alias" can be made in a table by using a metatable and a metamethod (which is covered in the next section).

local data = {number=42, text="The answer"}
data.answer = data.number -- Makes a copy, like with any other Number variable
data.answer = 44          -- Modifies the copy, original `number` remains intact

-- We need to leverage the __index metamethod to create a true alias
setmetatable(data, {
    __index = function(table, key)
        local lookup = {
            value = 'number',
            description = 'text',
        }
        local alias = lookup[key]
        if alias then
            -- Works, but it would call again this function, then the 2nd call
            -- would reach the end, return nil, and fallback to the real table.
            -- With rawget, we bypass that. Not strictly necessary though.
            -- return table.alias
            return rawget(table, alias)
        end
        -- If not found, we implicitly return nil, and that makes Lua look for
        -- the value in the object itself the usual way.
    end
})

print(data.value)
print(data.description)

inspect = require 'inspect'
print(inspect(data))

12. Metatables, metamethods and OOP

Note
There are many libraries for OOP and simulating classes. For many purposes, using them is probably the best option. The most popular seems to be middleclass, and seems perfectly fine. Also, worth checking Class Commons, a specification for a unified API for "class libraries" (to which middleclass adheres and is the most popular choices).

See Metatables and Metamethods in Programming in Lua for reference.

  • An empty table starts without metatable: t = {}; getmetatable(t) -→ nil

  • Any table can be metatable of any other table.

  • A group of related tables may share a table as metatable.

  • A table can be its own metatable.

  • Entries on a metatable of the appropriate name are metamethods, and start with a double underscore, which makes a bit more convenient that a table can be its own metatable (that way the names that Lua requires for metamethods don’t interfere with the names that one might want to use).

  • Important metamethods:

    • __add, __mul, __sub, __div…​

    • __tostring (used by print).

    • __call (allows to do table(), like overloading operator()()).

    • __metatable (if set to a value, prevents the metatable from being reset).

    • __index and __newindex are for failed table access, a.k.a. indexing. The former is for reading a field (access), the latter for writing (update). That is, a key in a table is not found, like in table['field']. It can be set to a function or a table. If the methamethod is a function, will be called with table as first argument, and 'field' as the second. If the metamethod is set to a table, it will delegate the lookup to that table (and if that table also fails, and has a metatable with __index, it will delegate recursively).

Metatables are used to implement object-oriented programming (among other things). The functionality and data which one object shares with others (because are from the same class, or have a shared base class), can be in a shared table. This is reachable from the actual object by Lua, if the object has a metatable with the __index metamethod defined.

Warning
One important caveat, is that the metamethods are only searched for directly into the metatable of the object, even if said metatable has an __index pointing to other table. Metamethods are not looked for in the second table, only regular field lookup. For example, if one adds __tostring to a table which is looked up for extra/shared fields, and that table is not the metatable of an object (it just happens to have functions or data reachable via __index), Lua will not call __tostring() on a print(object).

The following is an example of achieving basic object orientation using metatables and metamethods, and showing the problem of the metamethods only being accessible in the metatable, not in tables used indirectly via the indexing metamethods.

Note that it also shows two idiomatic ways of having a metatable. One, setmetatable(Base, Base), where a table is set to be its own metatable. The other, setmetatable(Derived, {__index = Base}), where the metatable is set to an anonymous ad-hoc table. The former benefits from being able to see more clearly what is on the metatable of a table. The latter benefits from having a cleaner table, which is not polluted with metamethods. The example also shows how the former style might be easier to add a new __call metamethod (just add to the table), while the latter requires either setting the metatable again, as it is not in scope directly (though could have been retrieved with getmetatable).

Base = {}
Base.sharedData = 42
function Base.__tostring()
    return "Base.__tostring"
end
setmetatable(Base, Base)
print(Base)

Derived = {}
setmetatable(Derived, {__index = Base})

print("## Field sharedData is available on Derived:")
print(Derived.sharedData)

print("## Derived is not pretty printed: __tostring is not on its metatable")
print(Derived)

print("## Function __tostring can be called manually thanks to __index, but it's not a metamethod")
print(Derived:__tostring())

setmetatable(Derived, {__index = Base, __tostring = Base.__tostring})
print("## Derived is now pretty printed: __tostring refers to Base.__tostring")
print(Derived) -- Works again

function Base.__call()
    print("Implementation from Base")
end
print("## Base now can be called like a function, but Derived can not")
Base()
-- Derived() -- Error, __call not found: __index doesn't help like with sharedData.
setmetatable(Derived, {__index = Base, __call = Base.__call})
print("## After setting the metamethod directly on the metatable, it works")
Derived() -- Now it works
Tip

Take into account this advice from Lua’s reference manual:

It is a good practice to add all needed metamethods to a table before setting it as a metatable of some object. In particular, the __gc metamethod works only when this order is followed (see §2.5.3). It is also a good practice to set the metatable of an object right after its creation.

— Lua 5.4 Reference Manual
https://www.lua.org/manual/5.4/manual.html

13. Iterators

A for statement like

for var_1, ···, var_n in explist do block end

is equivalent to the code:

do
  local ifunction, state, var = explist
  while true do
    local var_1, ···, var_n = ifunction(state, var)
    var = var_1
    if var == nil then break end
    block
  end
end

14. Changes in different editions

Lua 5.1 is the permanent interface for Nvim Lua. Plugins need only consider Lua 5.1, not worry about forward-compatibility with future Lua versions. If Nvim ever ships with Lua 5.4+, a Lua 5.1 compatibility shim will be provided so that old plugins continue to work transparently.
— Neovim's Lua introduction
https://neovim.io/doc/user/lua.html#lua-compat
Tip
seems that 5.1 is still today the preferred version for many reasons for "public" projects that might want broader compatibility with a Lua ecosystem.
Warning
When searching "lua variable arguments" on the web (or related queries), the 5.2 section of Programming in Lua comes up often on top. This is annoying, because that version of the book is for Lua 5.0, and variable arguments are much better in 5.1 (and the fact that the section happens to be 5.2 are a big distraction). Ignore that page entirely. There is a Stack Overflow answer explaining it very well.

14.1. On LuaJIT compatibility with PUC-Rio Lua

There’s no good reason to be fully compatible with Lua 5.3. Most Lua libraries you’d ever care about are compatible with 5.1. And LuaJIT already has the (few) relevant/useful features from 5.3. The poorly designed changes to the core numeric type will silently break lots of existing code. That’s not something I intend to emulate.

— Mike Pall
https://github.com/LuaJIT/LuaJIT/issues/576

The documentation on extensions explicitly states that LuaJIT is API + ABI-compatible with Lua 5.1. It adds a handful of upwards-compatible extensions from higher versions plus its own extensions.

Historically, Lua has broken the API, the ABI and existing Lua scripts on every minor 5.x release. And for no good reason. The language has not been evolved in forward-looking ways after Lua 5.1, the real issues of the language have not been tackled, the language has been severely fragmented due to the introduction of irrelevant or not-well-thought-out features and needless incompatibilities.

Apart from the extra performance that LuaJIT has to offer, developers prefer its stability guarantees and the prudent introduction of upwards-compatible extensions.

If you were to compare the actual user base of Lua 5.4 vs. the user base of LuaJIT, you’d quickly find out who successfully painted itself into a shrinking corner.

[Hints for LuaJIT: 92 million web sites (12/2022), huge SaaS installations, 50% global market share in network switches (2020), set-top boxes, embedded devices, FinTech, HPC, games, …​]

— Mike Pall
https://github.com/LuaJIT/LuaJIT/issues/929

No way. That feature is so misdesigned, it hurts.

Please look at the change history of Lua 5.4. After three years they are still playing the whack-a-mole game, trying to fix the collateral damage this caused everywhere in the VM (scopes, frames, coroutines, tailcalls, generic for loop, Lua/C API and so on). E.g. formerly lua_pop() was a simple pointer decrement (L→top--). Now, try to figure out what the current code expands to and start to cry.

First, the benefit for a new syntactic and semantic construct to provide locally scoped deterministic finalization in a garbage collected language is low. This mainly helps for toy programs or for initializer functions that set up multiple resources. The latter are few and far between — we coped just fine without all of that extravaganza. One still has to remember to explicitly close files, sockets and so on. In non-toy programs this usually happens in a completely different place, i.e. not in the same local scope. That feature doesn’t help you at all with this.

It helps to consider the equivalent feature in non-garbage collected languages (like C++) is 99% of the time used to deallocate memory (and not resources). Which is a complete non-issue in Lua.

Second, there’s already a perfectly working mechanism in Lua called a protected frame aka pcall(). In LuaJIT this is even zero-cost. The correct approach would have been to reuse that mechanism to avoid creating new intra-frame finalization semantics. And an explicit block syntax wouldn’t hurt, too. Because that Lua 5.4 feature right now creates an implicit semantic-block-but-not-really-a-block, which is not only hard to understand but is the root cause for most of its technical issues.

tl;dr: The cost-benefit ratio is very poor.

— Mike Pall
https://github.com/LuaJIT/LuaJIT/issues/929

But the semantics of this are not useful. Whether a coroutine is declared dead or not resumed anymore is irrelevant for the VM. If you consider the coroutine to be dead, then why bother calling a no-op function? Just remove the last reference to it and the GC will take care of it.

In other words, here’s the full implementation (for your personal use):

coroutine.close = function() end

— Mike Pall
https://github.com/LuaJIT/LuaJIT/issues/926

14.2. Integers in 5.3

Lua 5.3 added the integer subtype for numbers:

Things to note:

  • Both type(1) and type(1.0) yield the same: number.

  • Adding two integers yields an integer, but adding integer and real produces a real.

  • The division operator always produces a real: assert(3/2 == 1.5).

  • Use // for integer division: assert(3//2 == 1).

14.3. LuaJIT version and rolling release approach

This is a common source of confusion. I constantly have seen people claim that the project was unmantained, or in a dubious state due to the latest tag being a 2.1 beta for years. The issue is that Mike Pall, LuaJIT’s author and maintainer, moved LuaJIT to rolling releases. He basically considers that the tip of the branch is what should be ready to use at any given time.

After a lot of asking, he at least conceded to add to the build a time stamp that would monotonically increase, so versions can be compared. Here are some references and quotes from the discussions found online on the topic:

Distros asked for a kind of release number, because most of them are still stuck on the concept of releases.

Others asked for a monotonic version number, so they could ensure it has a fix they depend on. #862

The commit hash is not monotonic. One cannot tell whether X.Y.41c9 is after or before X.Y.3b0f.

That left me with the choice of either manually incrementing a number on every commit (which makes a mess in the git history) or to find some automated way to derive a monotonic version number. I’ve opted for the latter and the timestamp of the latest commit solves both constraints.

The status page explains how to get to the commit from the version number. So, please feel free to report a bug against either a version number or a commit — it’s easy enough to figure out. Strictly speaking, one should only report issues verified against the latest commit, anyway.

— Mike Pall
https://github.com/LuaJIT/LuaJIT/issues/1053

15. Neovim

  • Lua guide. The basics of using Lua in Neovim. Focus on the basics of how Lua files are loaded (paths, cache, modules, require, etc.), then the usage of typical editor functionality like variables, options, mappings and autocommands.

  • Lua introduction and concepts. It doesn’t cover the language, but mostly the integration into the editor.

  • Lua language reference documentation. An adaptation from the 5.1 reference manual in Lua.org.

16. LÖVE

16.1. Misc functions

  • love.graphics.setDefaultFilter(): use it to change the "filter" used when upscaling the simple graphics and fonts. For the "retro" feel, we don’t want the default (bilinear), which would smooth the image to not look pixelated. We want it pixelated, so we change it to 'nearest', which the nearest neighbor scaling (aka "Point scaling").

  • love.event.quit(): quit the application.

  • love.graphics.newFont(path, size): returns a new immutable font.

  • love.graphics.setFont(font): sets the current active font to the passed one.

16.2. Push library

It allows to create a "virtual" resolution or "game" resolution in which the game is designed and drawn to, then scaled back to the real window size. Helps in achieving the "retro" style, by pixelating things more.

It requires:

  • Use push:setupScreen(gameWidth, gameHeight, windowWidth, windowHeight) on love.load() to do the initial setup.

  • Adapt love.draw() to have push:apply('start') or push:start() as first thing called, then draw normally, and finally call push:apply('end') or push:finish() as the last thing.