Modest Attempt at Typesystem Augmenting the Lua Language — a subset of Haskell that compiles directly into a single Lua file with no external dependencies.
It targets Lua 5.4 and LuaJIT and loads like any other module — no separate runtime to ship, nothing for your host to link against. Mistakes are caught by the compiler before the host ever loads the code.
Get Started Try it in the Playground User guide GitHub
The compiler installs as the mll command. Install it from
crates.io:
cargo install mata-ll
Write some Haskell — fibs.mll:
fibs :: [Int] fibs = 1 : 1 : zipWith (+) fibs (tail fibs) main :: IO () main = mapM_ print (take 10 fibs)
Run it directly:
$ mll -r fibs.mll 1 1 2 3 5 8 13 21 34 55
Or compile it and require the resulting single
.lua file from any Lua 5.4 or LuaJIT host — see the
examples below.
local fib = require "fib"
local fibs = fib.fibonacci(8)
for i, n in ipairs(fibs) do
print(i, n)
end
fib :: [Int] fib = 1:1:zipWith (+) fib (tail fib) export fibonacci :: Int -> [Int] fibonacci = flip take fib
rr :: LuaIO "math.random" Number
rr2 :: Int -> Int -> LuaIO "math.random" Int
main :: IO ()
main = do
randNum <- rr
putStrLn $ "A number between 0.0 and 1.0: " <> show randNum
randNum2 <- rr2 23 42
putStrLn $ "An integer between 23 and 42: " <> show randNum2
local wf = require "writefibs"
local writer = function(fibString)
print("From mata-ll:", fibString)
end
wf.writeFibs(writer, 12)
export writeFibs :: (String -> LuaIO s ())
-> Int -> LuaIO s ()
writeFibs writer = loop 1 1
where
loop _ _ 0 = return ()
loop cur next count = do
writer (show cur)
loop next (cur+next) (count-1)
The type system is the point of the project. It provides:
Functor, Applicative, Monad and friends, with user-defined instances.nil.
In mata-ll, String is opaque — not [Char].
Treat it as a list and the compiler explains the deviation from GHC:
greeting :: String greeting = "Hello, " ++ "world" main :: IO () main = putStrLn greeting
Type error: Cannot unify '[a]' with 'String' at 2:10, in definition of 'greeting' note: in mata-ll String is not a list of characters — it is an opaque type that does not unify with [a]. A String cannot be passed where a list is expected, and list functions (++, map, length, …) do not accept it.
Forget a constructor and the pattern checker names the one you missed:
data Shape = Circle Number
| Square Number
| Rect Number Number
area :: Shape -> Number
area (Circle r) = 3.14159 * r * r
area (Square s) = s * s
Type error: Non-exhaustive patterns in 'area': missing patterns for Rect at 4:6, in definition of 'area'
Compiling a two-line hello.mll yields one self-contained
hello.lua: a small on-demand runtime followed by the compiled
definitions and an entry point. The part after the runtime:
main :: IO () main = putStrLn "Hello" >> putStrLn "World"
-- Generated by the mata-ll compiler (https://matall.org/)
local __MLLC_VERSION = "0.1.6"
local __MLLC_COMMIT = "da96b665cbc2a8db773c56f6ef387af7167446b8"
local __mll_fn = {}
__mll_fn[1] = function()
print("Hello")
return (print("World"))
end
local __mll_arg1 = ...
if __mll_arg1 == nil or (arg ~= nil and __mll_arg1 == arg[1]) then __mll_run(__mll_fn[1]()) end
Make available a useful subset of modern Haskell to Lua. Not intended as a replacement for Haskell, but as a way to write Haskell code where you would otherwise write Lua code. Primary focus: writing embedded code in a safer way than Lua allows, without breaking boundaries to Lua.
mata-ll uses non-strict evaluation, like Haskell. Function arguments and let bindings are not evaluated until their values are needed. This enables infinite data structures, avoids unnecessary computation, and behaves as Haskell programmers expect.
To avoid the overhead of thunking cheap expressions, the compiler performs cheapness analysis: expressions cheaper to compute than to thunk (arithmetic, variable references, literals, constructor applications) are evaluated eagerly. Only expensive expressions are wrapped in memoizing thunks.
For explicit control, seq :: a -> b -> b
forces evaluation of its first argument before returning the second.
While C may seem more portable, Rust is adding many targets, and keeping C out makes the build process more robust. The combination of Rust and Lua is a natural fit — mata-ll makes the Lua part more statically typed.
The project's purpose is to make Haskell available where it otherwise wouldn't be. Making GHC or another Haskell compiler a requirement would defeat that purpose. Rust avoids Haskell's large ecosystem, dependency issues, and enormous binaries while keeping the compiler itself reliable and embeddable.
Each .mll file is a module. When compiling, included
.mll files are merged into the resulting output
.lua file.
No additional runtime required. Types deriving
LuaDict form the interop surface: records become
string-keyed Lua tables, nullary enums become strings. Other
algebraic types use an internal representation.
An FFI interface is provided for calling into Lua and for exporting functions to Lua. Both directions use the same mechanism.
This project was developed collaboratively by a human and an AI. The design, direction and taste are Hans-Christian's; much of the implementation was written by Claude (Anthropic). Neither could have built it alone — at least not in a weekend.