Macros (defmacro)

Turmeric macros are compile-time functions from syntax to syntax. A defmacro runs during elaboration, receives its arguments as unevaluated forms, and returns a form; the returned form then flows through the FULL elaborator -- type checking, typeclass dispatch, effect rows, refinements, borrow checking, everything. Macro-generated code is not a second-class citizen: it is checked exactly like code you wrote by hand, in both engines (compiled and --interpret).

(defmacro twice [e] `(+ ~e ~e))

(twice 21)       ; expands to (+ 21 21) => 42
(twice "oops")   ; expands to (+ "oops" "oops") => TUR-E0006 type error

That second line is the load-bearing property: the expansion is wrong, and the TYPE CHECKER says so. When a diagnostic points into generated code, the compiler appends a note naming the call you actually wrote:

error [TUR-E0006]: operator lookup failed for '+': ... first arg type cstr
note: in expansion of macro 'twice' -- the diagnostics above are inside
      code this call generated

Every core control-flow form you use daily -- cond, when, for, do-m, the contract forms -- is a defmacro in stdlib/macros.tur; read that file for a working style reference.

Quasiquote, unquote, splice

Syntax Meaning
`form quasiquote: build the form as a template
~e unquote: substitute the value of e into the template
~@e unquote-splicing: splice a list's elements into the surrounding form

Splicing works into calls, vectors, and (via structural recursion) any depth of the template. A macro can also call functions and other macros inside a splice expression, so a template can generate its spliced sequence rather than merely forwarding one.

Hygiene: deliberately manual

Turmeric macros are unhygienic by design (Common-Lisp style): a binding introduced by a template captures a same-named binding at the call site.

(defmacro bad [e] `(let [tmp 99] (+ tmp ~e)))
(let [tmp 1] (bad tmp))    ; => 198, NOT 100: the template's tmp captured yours

The discipline is gensym: mint a fresh symbol for every binding a template introduces.

(defmacro good [e]
  (let [t (gensym "tmp")]
    `(let [~t 99] (+ ~t ~e))))
(let [tmp 1] (good tmp))   ; => 100

Rule of thumb: any let, fn parameter, or loop variable your template creates gets a gensym. Capture is occasionally what you want (anaphoric macros); when you rely on it, say so in the macro's docstring.

gensym freshness is real, not just a counter: a candidate name is checked against the symbol table -- every symbol the reader has seen, including the whole current file -- and already-interned names are skipped. So a hand-written tmp_0 in your code cannot be captured by (gensym "tmp"); the macro simply receives tmp_1 (or later).

(gensym) only means something inside macro machinery (templates and quasiquote), where it is expanded at compile time. In ordinary runtime code it is a hard error -- a fresh symbol has no runtime value.

^syntax parameters: receiving raw AST

By default a macro argument is substituted into the template as code. A parameter marked ^syntax instead binds the raw, unevaluated form, so the macro body can WALK it with the compile-time builtins:

(defmacro field-name [^syntax decl]
  `(println ~(symbol-name (first decl))))

^syntax composes with variadic rest params (& ^syntax decls), which is the workhorse for "iterate over my arguments" macros.

Recursion over arguments

A variadic macro recurses over its rest list with first / rest, using empty? as the base case:

(defmacro sum-all [& ^syntax xs]
  (if (empty? xs)
    `0
    `(+ ~(first xs) (sum-all ~@(rest xs)))))

(sum-all 1 2 3 4 5)   ; => 15

Two traps, both of which end in "maximum macro expansion depth exceeded" (the cap is 256):

The compile-time builtin set

Inside a macro body (outside templates, and inside ~/~@ escapes) the compile-time evaluator provides:

Plus calls to other macros and to compile-time-evaluable functions inside splices. That is the whole set -- notably absent: arithmetic, string comparison beyond =, and any type inspection (see Limitations). The set is defined once, in CT_BUILTIN_TABLE in src/compiler/elab_macros.c, which drives both the evaluator's dispatch and the decision to route a substituted template through the evaluator at all.

One subtlety of that routing: a template runs through the compile-time evaluator when it still carries quasiquote machinery or calls one of the builtins above -- EXCEPT = and not, which appear in templates that are pure runtime code and therefore do not trigger evaluation on their own. A macro body whose only compile-time work is =/not/if should thread it through a form that does trigger (in practice any real body has a quasiquote, which always triggers).

Procedural macros (defmacro*)

Where a defmacro body is a template, a defmacro* body is ordinary Turmeric, evaluated at expansion time by the in-process interpreter (the macro-time env). Each parameter arrives as a Syntax value wrapping the raw call-site form; the body computes with the full language -- arithmetic, strings, recursion, local functions -- plus the syntax vocabulary (read-string, syntax-first/rest/nth/len/tag, syntax->int / int->syntax and friends, syntax-list/vec/cons, syntax-gensym, syntax=?, syntax-error), and returns the expansion as a Syntax.

(defmacro* const-sum [a b]                 ; compile-time arithmetic --
  (int->syntax (+ (syntax->int a)          ; impossible in a template
                  (syntax->int b))))

(const-sum 40 2)   ; compiles to the literal 42

(defmacro* twice [e] `(+ ~e ~e))

(twice (f))        ; expands to (+ (f) (f))

Quasiquote inside a defmacro* body is sugar for the syntax constructors -- ~expr splices a Syntax-valued expression (any computation, not just a parameter), and ~@expr splices the elements of a list-shaped Syntax:

(defmacro* sum-first-last [& xs]
  `(+ ~(syntax-first xs) ~(syntax-nth xs (- (syntax-len xs) 1))))

(defmacro* call-all [f & xs] `(~f ~@xs 100))
(call-all add3 1 2)   ; expands to (add3 1 2 100)

The lowering is purely syntactic (`(+ ~e ~e) becomes (syntax-list (sym->syntax "+") e e)), so the body stays ordinary typed Turmeric. Nested quasiquote and ~@ into vector templates are not supported -- build those with the constructors.

Facts that matter in practice:

Macro-time imports (:for-macros)

(import m :for-macros) inside a defmodule evaluates module m into the macro-time env, so defmacro* bodies can call its functions at expansion time -- shared macro-time helper libraries:

;; mhelp.tur
(defmodule mhelp
  (export mx-add)
  (defn mx-add [a : int b : int] : int (+ a b)))

;; main.tur
(defmodule prog
  (import mhelp :for-macros)
  (defmacro* csum [a b]
    (int->syntax (mx-add (syntax->int a) (syntax->int b))))
  (defn main [] : int (println (csum 40 2)) 0))   ; compiles to 42

The rules:

Bounded type reflection (syntax-struct-fields)

(syntax-struct-fields T) -- available only inside a running macro expansion -- takes a Syntax symbol naming a single-constructor record (a defstruct) and returns its field names as a Syntax list of symbols. This is the R3-sanctioned shape of type reflection: a bounded, total, flat projection of the compile's registry; no Type value ever becomes a macro-time value. It is what lets a derive macro take just the type:

(defmacro* derive-show3 [TypeName]
  (letrec [fields (syntax-struct-fields TypeName)
           ...]
    `(definstance Show [~TypeName] ...)))

(derive-show3 P3)   ; no field list -- see tests/fixtures/macro-reflect-derive/

An unknown name, an opaque newtype, or a multi-constructor/positional data type is a plain expansion-time diagnostic ("walk its variants explicitly").

Procedural reader macros (by composition)

A reader macro whose template expands into a defmacro* call gives the read-time syntax a full-language expander -- the RM5 "function expanders" plan point, delivered by composition instead of a second mechanism:

(defmacro* csum* [& xs] ...compile-time fold...)
(reader-macros/define 'csum :datum-bracket '(csum* $body))

(println #csum[1 2 3 4 5])   ; compiles to the literal 15

See tests/fixtures/reader-macros-procedural/.

Effectful macros (--macro-caps=io)

Macro-time code runs with every capability denied. For the rare legitimately-effectful macro (an embed-file style generator), the global flag --macro-caps=io re-grants exactly I/O; anything else -- ffi, unsafe, inline-C, async -- is refused by the flag parser and never available at expansion time. Without the flag, an I/O call in a macro body is a plain expansion-time diagnostic (tests/fixtures/errors/macro-io-denied/). - Unhygienic like defmacro -- mint bindings with syntax-gensym.

Prefer a plain defmacro template when substitution is all you need; it expands without spinning the interpreter. Reach for defmacro* the moment you need computation the CT evaluator refuses (counting, arithmetic, string synthesis, structural analysis).

Multi-form bodies

A defmacro body may be several forms: every form before the last is compile-time SETUP, evaluated in order by the compile-time evaluator's do (with (def x v) spliced into let bindings), and the LAST form's value is the template.

(defmacro square-sum [a b]
  (def a2 (list * a a))
  (def b2 (list * b b))
  `(+ ~a2 ~b2))

(square-sum 3 4)   ; => 25

A multi-form body always runs through the compile-time evaluator -- a (do setup... template) sequence has no meaning as a literal template.

Generating names, and whole typed declarations

symbol-name + str-append + str->sym synthesize identifiers, and a single macro invocation can emit SEVERAL top-level definitions by wrapping them in (do ...) -- top-level do splices into the program:

(defmacro def-record [name T]
  `(do
     (defstruct ~(str->sym (str-append (symbol-name name) "Rec"))
       [val : ~T count : int])
     (defn ~(str->sym (str-append (symbol-name name) "-mk")) [v : ~T]
       : ~(str->sym (str-append (symbol-name name) "Rec"))
       (make-struct ~(str->sym (str-append (symbol-name name) "Rec")) v 1))))

(def-record Score :float)
;; defines struct ScoreRec [val : float count : int]
;; and (Score-mk 7.1) : ScoreRec

Types in templates: full power in the write direction

Type positions are ordinary syntax at expansion time, so a macro can interpolate ANY type -- simple, compound, or synthesized -- into any type position, and the checker resolves it downstream:

;; a passed-in type token
(defmacro typed-id [T] `(fn [x : ~T] : ~T x))
((typed-id :int) 42)

;; compound types, one template -> several typed instantiations
(defmacro defvecfn [name T dflt]
  `(defn ~name [v : (Vec ~T)] : ~T
     (if (> (vec-len v) 0) (vec-get v 0) ~dflt)))
(defvecfn first-int :int 0)
(defvecfn first-str :cstr "none")

;; synthesized names resolve (and FAIL like hand-written types when wrong)
`(defn make-it [] : ~(str->sym (str-append (symbol-name prefix) "Bar")) ...)

This extends to ascriptions ((:: e (Vec ~T))), defstruct field types, inline-C-adjacent signatures, and even #row{...} elements (#row{~T int}). A synthesized type that resolves to nothing is a real elaboration error, not silent acceptance -- the checker genuinely evaluates what you spliced.

The one thing a macro CANNOT do with types is the read direction: see Limitations.

Emitting inline-C

A template may contain an inline-C block; the enclosing defn form is emitted like any other. Remember the repo style rule: the closing ``` and its ) stay on one line (```)).

Limitations, stated plainly

These are design boundaries, not bugs. Each has a written rationale in row-types-followups-plan.md (R3) and the documents it cites; the forward direction -- procedural macros running the full language on turi, with this evaluator frozen -- is macro-system-direction-plan.md.

Debugging expansions

History

The macro system's sharp edges were filed and fixed as they were hit; the paper trail is in docs/archive/history/ (splice-into-vector, unquote in type position, inline-C emission, multiple top-level forms, compile-time calls in splices, ^syntax parameters, and more). If a limitation you hit is not listed above, check there before assuming it is by design.