Building DSLs

29.1 Why Nim Excels at DSLs

Flexible syntax (command calls, do blocks, operator definitions), untyped macro arguments, compile-time code generation, and zero runtime overhead.

29.2 Example: an HTML DSL

import std/macros
macro html(body: untyped): string =
  proc build(n: NimNode): NimNode =
    case n.kind
    of nnkCall, nnkCommand:
      let tag = $n[0]
      var inner = newLit("")
      for i in 1 ..< n.len:
        let child = if n[i].kind == nnkStmtList: n[i] else: newStmtList(n[i])
        for c in child:
          inner = infix(inner, "&", build(c))
      result = infix(infix(newLit("<" & tag & ">"), "&", inner), "&", newLit("</" & tag & ">"))
    of nnkStrLit: result = n
    else: result = prefix(n, "$")
  result = newLit("")
  for stmt in body: result = infix(result, "&", build(stmt))

echo html:
  ul:
    li "one"
    li "two"
# <ul><li>one</li><li>two</li></ul>

29.3 Real-World DSL Examples

Karax (VDOM HTML DSL), Jester routes (routes: get "/": resp "hi"), Nimble tasks, unittest suite/test, std/strscans (scanf), pattern matching (fusion/matching), Arraymancer tensor slicing syntax, npeg (PEG parser generator DSL), cligen (auto-CLI from proc signatures).

29.4 Term-Rewriting Macros and Patterns (Advanced, Rarely Used)

Term-rewriting macros (TRMs) are experimental and discouraged for general code.

template optMul{`*`(a, 2)}(a: int): int = a + a     # rewrite x * 2 → x + x