More Lua DSL Stuff

last updated 2026-10-29

If you just want the code, click here

Continuing off my previous DSL building in Lua and finally finishing PROCESSED WORKMEAT, I took some time to explore more ideas around DSLs in Lua. The previous article is a pre-req for this one, as I'm not going to rehash the whole article. The main point of this article is to jot out my current results on bindings in Lua-based DSLs.

Bindings

Bindings were something that I didn't have a satifying approach for. Some of my early sketches involved code with strings and explicit getters. However, this was rather ugly. Producing code that had too much nesting such as multiply (get "x") .by (get "y"). It'd be much nicer if I could instead write multiply (x) .by (y) directly.

So, I figured out how to do that with some _G hacking.

setmetatable (_G, { __index = function (t, k)
    local value = rawget (t, k)
    if value then
        return value
    end
    -- This could be cached.
    -- But for now, generate garbage.
    return {
        name = k ;
        function (scope)
            return scope [k]
        end ;
    }
end})

The afformentioned _G hack.

The above code assigns a metatable to _G that will spit out a binding object any time a global variable has no definition. This binding object does one thing when evaluated: it looks up a variable in the provided scope. It's a simple trick that gives me the power to define set (...) .to (...) as the following:

local set = function (var)
    return {
        to = function (val)
            return function (scope)
                scope[var.name] = eval (val, scope)
            end
        end
    }
end

Implementation of set

Note that set (...) .to (...) has a call to eval. This is because val could also be a binding object, a block of code, a function to run, or simply a value. Eval handles unwrapping a computation for its value if needed:

local eval = function (program, scope)
    if type (program) == "function" then
        return program (scope)
    elseif type (program) == "table" then
        local value = nil
        for _, action in ipairs (program) do
            value = action (scope)
        end
        return value
    else
        return program
    end
end

The implementation of eval

We also added a wrapper called run which will call eval with a fresh scope. The previous scope (if it exist) is deligated to for variables that don't exist in our current scope using __index.

local run = function (program, previous_scope)
    local scope = setmetatable ({}, {__index = previous_scope})
    return eval (program, scope)
end
Implementation of run

With our interpreter implemented, we can define some additional operations and then implement a simple test program to see bindings in action:

local show = function (expression)
    return function (scope)
        print (run (expression, scope))
    end
end

local multiply = function (a)
    return {
        by = function (b)
            return function (scope)
                return eval (a, scope) * eval (b, scope)
            end
        end
    }
end

local add = function (a)
    return {
        with = function (b)
            return function (scope)
                return eval (a, scope) + eval (b, scope)
            end
        end
    }
end

local sqrt = function (x)
    return function (scope)
        return math.sqrt (eval (x, scope))
    end
end

run {
    set (x) .to (3) ;
    set (y) .to (4) ;
    show {
        set (x_sq) .to (multiply (x) .by (x)) ;
        set (y_sq) .to (multiply (y) .by (y)) ;
        sqrt (add (x_sq) .with (y_sq)) ;
    } ;
}

A set of operations and a program using them to compute the magnitude of the vector (3,4)

What's Left

I'm slowly understanding how monads and other "computation as data structures and functions" work. The eval process this time around is much more consistent and feels a lot cleaner. The above could be improved by explicit scope management instead of implicit management (like how show uses run instead of eval. Tho that was mostly to test if scoping worked).

Also, I am really appreciating how powerful and flexible Lua can be when you let your program take ownership of the whole language. For me, Lua is a systems language. It's the primary way you build everything in LÖVE. I'm often interfacing with my systems through their raw APIs. It turns every program into a tedious, error prone, and complex assembly language pretending it's a high-level language. Boilerplate as the say. So, being able to create new scripting languages without having to implement a parser is a major boon.

It's nice re-learning how to write actually high-level code. Instead of low-level code with a roomba (the GC).

I think my next iteration would be making this more concatenative. I really hate nesting function calls. I'd rather express that final operation as:

multiply (x) .by (x) ;
multiply (y) .by (y) ;
add (_) .with (_) ;
sqrt (_) ;

A version where there is a stack of values between operations. _ pulls a value from the stack.

Or perhapse:

x ; dup ; multiply ;
y ; dup ; multiply ;
add ; sqrt ;
A version that is even more stack-based ;)

But that is future work for a future and.