lunacy

Lunacy is a fork of Lua 5.1.5 (why 5.1? Because it’s about 20% smaller than Lua 5.3 and because there’s a lot of code based on Lua 5.1: Roblox Luau, LuaJIT, Gopher Lua, Adobe Lightroom Classic, etc.) designed to be a tiny yet powerful stand alone scripting language.

The reason for this fork is to have a security hardened version of Lua 5.1.

The main differences from Lua 5.1 are:

Lunacy documentation

I have written a PDF book on how to program in Lunacy; while this doesn’t cover everything Lunacy can do, it covers the kind of text processing I used to do in Perl. Source files for this book are also available.

There is also a PDF reference manual which covers pretty much everything Lunacy can do. This manual is also available in other formats.

OS support and getting Lunacy

This can be compiled as a tiny Windows binary, and it also compiles and runs in Linux. Since Lunacy is written in pure C, it should be able to compile pretty much anywhere else, but this has not been tested.

Lunacy is available at GitHub, Sourcehut, and Codeberg.

Lunacy has a web page.

Compiling lunacy

To compile, one needs a POSIX standard make program and a C compiler with the name gcc. If one wishes to use another C compiler, edit the file Makefile and change the line CC= gcc to use the compiler in question.

To compile on a Linux or compatible system (e.g. Cygwin) with readline support (so, when invoked from a terminal, one has arrow history), enter the src/ directory and invoke the make command as follows:

        make -f Makefile.readline

Note that the resulting binary will be GPL licensed. If this is not desired, and arrow history is wanted, Lunacy also has support for editline. To compile Lunacy with editline support, after installing editline:

	make -f Makefile.editline

To compile this on a Mingw system:

        make -f Makefile.mingw32

To compile this on another system:

	make

The code is compatible with gcc (gcc 3.4.2 and gcc 11.3.0), clang (clang 8.0.1), and will probably compile in other compilers, including C++ compilers, without issue, e.g. it has been compiled and runs with tcc (tcc 0.9.25 for Windows).

If using another name for the Makefile, e.g. Makefile.foo (which would be invoked as make -f Makefile.foo), be sure to edit the Makefile used and change the line which sets its MAKEFILE value.

Lunacy changes from Lua 5.1

Changelog (Luancy binary only)

SipHash

Lunacy, by default, uses HalfSipHash-1-3 as its hash compression algorithm. This has reasonable security, while being very fast when Lunacy is compiled as a 32-bit or 64-bit binary.

As a result, pairs() is no longer deterministic. Let’s look at this code

#!/usr/bin/env lunacy
foo = {a=1, b=2, c=3, d=4, e=5}
out = ""
for a in pairs(foo) do
  out = out .. a .. " "
end
print(out)

In Lua 5.1, the above will generate the same output over and over (in Lua 5.4.4 and LuaJIT, multiple runs of the above code will generate different outputs); in Lunacy, each invocation of the above code will generate a different output.

In cases where we need a deterministic pairs(), see sPairs() below.

If running on a 64-bit system, it may be desirable to compile Lunacy to use 64-bit SipHash-1-3. To do so, edit src/Makefile to add the flag -DFullSipHash, e.g.:

CFLAGS= -O3 -Wall $(MYCFLAGS) -DFullSipHash

To use 64 bit SipHash-2-4, likewise add -DSIP24:

CFLAGS= -O3 -Wall $(MYCFLAGS) -DFullSipHash -DSIP24

Why HalfSipHash-1-3 is the default

I have run a number of benchmarks with Lunacy, my fork of Lua 5.1, to see how much changing the SipHash variant used affects performance, for both 32-bit (386) and 64-bit (x86_64) binaries.

Conclusion: I will use HalfSipHash31 as the hash compression algorithm, for both 32-bit and 64-bit builds of Lunacy.

The binaries have been compiled using GCC 8.3.1, in CentOS 8, using an older Core Duo T8100 chip from 2008. The benchmark consisted of loading and processing a bunch of COVID-19 data in to large tables taking up 550 (32-bit) or 750 megs (64-bit) of memory. This real-world benchmark (it is the exact same code I use to build an entire COVID-19 tracking website) was done multiple times, to minimize speed fluctuations from outside factors, against the following setups:

And the following string hash compression functions:

Here are the results, where lower numbers are better (less time needed to run the benchmark):

lunacy64-noSipHash 197.801
lunacy64-sipHash13 203.457
lunacy64-SipHalf13 203.507
lunacy64-sipHash24 210.043
lunacy32-noSipHash 240.898
lunacy32-SipHalf13 246.995
lunacy32-sipHash13 265.916
lunacy32-sipHash24 270.226

HalfSipHash-1-3 is as fast as full SipHash-1-3 on 64-bit CPUs, while being quite a bit faster for 32-bit binaries compared to 64-bit sipHash.

HalfSipHash-1-3 is only 2.5% slower on 32-bit machines (compared to Lua’s “stock” hash); it is only 2.9% slower on 64-bit machines.

In Lunacy’s use case, HalfSipHash should provide an adequate security margin; as per what its designer has to say:

HalfSipHash takes its core function from Chaskey and uses the same construction as SipHash, so it should be secure. Nonetheless it hasn’t received the same amount of attention as 64-bit SipHash did. So I’m less confident about its security than about SipHash’s, but it obviously inspires a lot more confidence than non-crypto hashes.

Too, HalfSipHash only has a 64-bit key, not a 128-bit key like SipHash, so only use this as a mitigation for hash-flooding attacks, where the output of the hash function is never directly shown to the caller.

sPairs()

Since pairs() is non-deterministic, in cases where it’s desirable to iterate through a table in a consistent manner, I have this public domain Lua/Lunacy code to sort the elements of a table when iterating through it:

function sPairs(inputTable,sFunc)
  if not sFunc then
    sFunc = function(a, b)
      local ta = type(a)
      local tb = type(b)
      if(ta == tb)
        then return a < b
      end
      return ta < tb
    end
  end
  local keyList = {}
  local index = 1
  for k,_ in pairs(inputTable) do
    table.insert(keyList,k)
  end
  table.sort(keyList,sFunc)
  return function()
    rvalue = keyList[index]
    index = index + 1
    return rvalue, inputTable[rvalue]
  end
end

This code is run the same way one runs pairs(); for example:

foo = {a=1, b=2, c=3, d=4, e=5}
out = ""
for a in sPairs(foo) do
  out = out .. a .. " "
end
print(out)

See also

Some other languages based on Lua:

Other embedded languages:

LuaJIT

LuaJIT is a high performance implementation of Lua 5.1.

Since development for LuaJIT has slowed down, there are some forks of it:

AI statement

No AI was used in the development of Lunacy, nor to help with writing the documentation for Lunacy.