mata-ll

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

Getting Started

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.

Calling from Lua into mata-ll

callfib.lua
local fib = require "fib"

local fibs = fib.fibonacci(8)
for i, n in ipairs(fibs) do
    print(i, n)
end
fib.mll
fib :: [Int]
fib = 1:1:zipWith (+) fib (tail fib)

export fibonacci :: Int -> [Int]
fibonacci = flip take fib

Calling Lua library functions from mata-ll

random.mll
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

Passing Lua callbacks to mata-ll

callwritefibs.lua
local wf = require "writefibs"
local writer = function(fibString)
    print("From mata-ll:", fibString)
end
wf.writeFibs(writer, 12)
writefibs.mll
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

The type system is the point of the project. It provides:

What the compiler catches

In mata-ll, String is opaque — not [Char]. Treat it as a list and the compiler explains the deviation from GHC:

greeting.mll
greeting :: String
greeting = "Hello, " ++ "world"

main :: IO ()
main = putStrLn greeting
compiler output
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:

area.mll
data Shape = Circle Number
           | Square Number
           | Rect Number Number

area :: Shape -> Number
area (Circle r) = 3.14159 * r * r
area (Square s) = s * s
compiler output
Type error: Non-exhaustive patterns in 'area':
  missing patterns for Rect
  at 4:6, in definition of 'area'

What the output looks like

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.


Project Goals

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.

Evaluation Strategy

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.

Why Rust?

Why not C?

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.

Why not Haskell?

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.

Language Properties

.mll files

Each .mll file is a module. When compiling, included .mll files are merged into the resulting output .lua file.

Lua interop

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.

FFI

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.