Skip to content
AgileLogics
5 min read

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

  • __index on 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.