1. Misc links
-
https://en.wikipedia.org/wiki/Category:Lua_(programming_language)-scripted_video_games
-
On Lua lacking "batteries", unlike Python. About the lack of standard libraries with the core. The article contains some interesting comments on how Lua is embedded in some projects.
-
OOP Primer. Implements most object oriented techniques (including private members, overriding and inheritance) without having to even use metatables, and in very simple terms. Worth checking out to compare with the use of them.
-
https://www.reddit.com/r/neovim/comments/106kks4/psa_neovim_is_now_shipped_with_an_introductory/
-
https://github.com/medwatt/Notes/blob/main/Lua/Lua_Quick_Guide.ipynb
-
https://github.com/mfussenegger/nlua Lua script that let’s you use Neovim as a Lua interpreter. Allows you to run tools like busted to test Neovim plugins.
-
https://gist.github.com/tylerneylon/81333721109155b2d244 A very elegant overview of copying a table in Lua. Goes from the simplest approach of handling tables besides values to do the deep copy, to adding support for metatables (just one extra line) or supporting cycles between tables.
-
https://www.love2d.org/forums/viewtopic.php?t=66975 A forum post about doing a deep copy of an object using the
middleclass
library. The author of the library advises to just use the constructor of the class to make a duplicate of itself. This is way simpler than a deep copy function (that probably should be very careful with the metatables and cycles), and can support side effects that a constructor might want to do. -
https://github.com/leafo/tableshape A library for defining a schema for tables, validate its contents, and transforming them if necessary.
1.1. Compiling to a single executable
-
Compile Lua scripts to a library that can be linked into your executable. It links to LuaJIT’s page on an option to compile in different ways which are worth exploring. It seems it might be possible to compile a whole library in a portable bytecode that would be loaded as if it were source.
The following are archived, and/or obviously/explicitly unmaintained:
-
https://github.com/LuaDist/squish Build a single file from multiple scripts and modules
-
https://github.com/ers35/luastatic Builds a standalone executable from a Lua program.
-
https://github.com/Frityet/combustion Compile a Lua project to a self contained executable.
-
https://github.com/tongson/omnia "Batteries included Lua", points to omniajit
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:
-
Some ideas ot make constants and protect the global scope: https://stackoverflow.com/questions/12368623/is-there-any-keyword-like-const-or-anything-else-which-does-the-same-job-with-it/
-
One more from Redis: https://stackoverflow.com/questions/19997647/script-attempted-to-create-global-variable
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 endtranslates to
f = function () body endThe statement
function t.a.b.c.f () body endtranslates to
t.a.b.c.f = function () body endThe statement
local function f () body endtranslates to
local f; f = function () body endnot 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 endis 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.
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 byprint
). -
__call
(allows to dotable()
, like overloadingoperator()()
). -
__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 intable['field']
. It can be set to a function or a table. If the methamethod is a function, will be called withtable
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:
— 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
-
https://luajit.org/extensions.html Covers the features that LuaJIT supports from 5.2 (some unconditionally, some with a compile-time switch).
-
https://luau-lang.org/compatibility Shows the list of issues that newer Lua versions might have and which Luau developers disagree with
-
What’s new in Lua 5.4. Includes links to several topics on compatibility across versions, including LuaJIT, and links to comments from Mike Pall.
-
Determine Lua version. A pretty mind blowing way to determine the Lua version without relying on anything that could be changed, just with difference in behavior.
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.
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.
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, …]
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.
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
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)
andtype(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.
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
-
https://github.com/kikito/anim8 Animation library (sprite sheets, quads, etc.)
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)
onlove.load()
to do the initial setup. -
Adapt
love.draw()
to havepush:apply('start')
orpush:start()
as first thing called, then draw normally, and finally callpush:apply('end')
orpush:finish()
as the last thing.