Building Domain-Specific Languages
Prerequisites: Environments and sandboxing, Metatables and metamethods
Lua's syntax sugar — calling a function with a table or string without parentheses — and its environments make it an excellent base for configuration languages and small DSLs. Lua began life as exactly that: a data-description language.
Declarative configuration
local servers = {}
local function server(def)
assert(def.name, "server needs a name")
def.port = def.port or 80
servers[#servers + 1] = def
end
local config = [[
server { name = "web", port = 8080, routes = {"/", "/about"} }
server { name = "api", routes = {"/v1"} }
]]
local env = {server = server}
assert(load(config, "config", "t", env))()
for _, s in ipairs(servers) do
print(s.name, s.port, table.concat(s.routes, " "))
end
web 8080 / /about
api 80 /v1
server { ... } is just a function call with a table argument, but it
reads like a custom config format. Running the config in a sandbox (only
server is available) keeps it safe. Tools like Premake, xmake,
and many game engines use this approach for build and level files.
Builder DSLs with chaining
local function tag(name)
return function(arg)
local attrs, children = {}, {}
if type(arg) == "string" then children = {arg}
else
for _, child in ipairs(arg) do children[#children + 1] = child end
for k, v in pairs(arg) do
if type(k) == "string" then attrs[#attrs + 1] = string.format(' %s="%s"', k, v) end
end
table.sort(attrs)
end
return "<" .. name .. table.concat(attrs) .. ">" .. table.concat(children) .. "</" .. name .. ">"
end
end
local div, h1, p, a = tag "div", tag "h1", tag "p", tag "a"
print(div { class = "card",
h1 "Lua DSLs",
p { "Read the ", a { href = "/docs", "docs" }, "." },
})
<div class="card"><h1>Lua DSLs</h1><p>Read the <a href="/docs">docs</a>.</p></div>
Each tag is a function returning a function; string and table arguments need no parentheses, so the Lua code mirrors the HTML structure.
Taking it further
__indexon the environment can create tags or keywords on demand, so authors never declare them.- LPeg builds real parsers when you need your own syntax rather than Lua's.
- Always validate DSL input and report errors with the user's line
numbers —
load's chunk name argument appears in error messages.