Tln does metaprogramming the Elixir way: macros are code that writes code, and they run at compile time. A defmacro expands into ordinary blocks before validation and planning — so the validator, planner, and runtime never see a macro, and the engine stays exactly as deterministic and terminating as always. The one place unbounded computation is allowed is expansion itself, bounded by a step budget (a compile error, never a runtime hang). That’s also why macros live in core, not a plugin: only core owns the grammar and the compile phases.

Prolog can metaprogram too — it’s homoiconic — via the classic term_expansion/2 hook (and its runtime cousins =.., call, assert). So this is less “Prolog can’t” than “Tln does it differently”: quote turns a block into an AST value, unquote splices a value in, and defmacro is a compile-time function from arguments to AST. Here one macro kills the boilerplate across near-identical detect rules:

ISO Prolog (1995)
% Compile-time metaprogramming via term_expansion/2:
% each over_threshold/3 fact is rewritten, as it is read,
% into a high/1 rule.
term_expansion(over_threshold(Name, Metric, Limit),
               ( high(Item) :-
                    record(_, Item, item, _, _, _),
                    attr(_, Item, Metric, V),
                    V > Limit )).

over_threshold(temperature, temp_c, 80).
over_threshold(pressure,    psi,   200).
Tln
defmacro over_threshold(name, metric, limit, prio) {
  quote {
    detect "High {unquote(name)}" {
      for records where type == "item"
        and attr unquote(metric) > unquote(limit)
      flag matching items
      label "{item.name}: high {unquote(name)}"
      priority unquote(prio)
    }
  }
}

over_threshold("temperature", "temp_c", 80, HIGH)
over_threshold("pressure",    "psi",   200, MEDIUM)

The difference is what they rewrite and when. Prolog’s term_expansion (and =.. / call) operate on Prolog terms — function symbols that flat-EAV Tln core doesn’t have, so that runtime “code-as-data” belongs to the tln-prolog engine. Tln’s macros expand to AST blocks at compile time, leaving the runtime a pure, deterministic Datalog. The macro above expands into exactly two ordinary detect blocks — all the validator, planner, and runtime ever see:

detect "High temperature" {
  for records where type == "item" and attr "temp_c" > 80
  flag matching items
  label "{item.name}: high temperature"
  priority HIGH
}
detect "High pressure" {
  for records where type == "item" and attr "psi" > 200
  flag matching items
  label "{item.name}: high pressure"
  priority MEDIUM
}