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 the five-line fib.mll above yields one self-contained
fib.lua: a small on-demand runtime (thunks, cons cells,
show) followed by the compiled definitions and an export table.
The interesting part:
-- Generated by MATA-LL compiler (https://matall.org/)
fib = __mll_lazy_cons(1, function() return __mll_lazy_cons(1, function() return zipWith(function(_a, _b) return __force(_a) + __force(_b) end, fib, __thunk(function() return (tail(fib)) end)) end) end)
-- Exports
return {
fibonacci = function(a1)
local __result = __force(__mll_fn[15])(a1)
if type(__result) == "function" then __result = __result() end
return __mll_to_lua(__result)
end,
}
The infinite list survives translation — laziness is compiled in, not simulated by a runtime. Exports deep-force their results into plain Lua tables, so the host sees ordinary values.
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.