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 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:

hello.mll
main :: IO ()
main = putStrLn "Hello" >> putStrLn "World"
hello.lua
-- 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

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.