Metacall: metaprogramming in Logos
Metacall is Logos’s metaprogramming system. Its one idea is that a metaprogram is an ordinary Logos function — same body language, same types, same traits, same let/match/if — that the compiler runs at compile time in its JIT, and whose result splices back into your program. There is no separate macro language, no second evaluator, no second type system, and no second set of safety rules. Code that writes code is just Logos code that happens to run during compilation.
Status: the substrate described here — the metacall keyword, #[fn_macro] / #[token_macro], #[metaprog_handler], and the quote_*! forms — is implemented and shipping. A more ambitious capability-gated metafunction model is designed but not built; see What ships, what’s designed below and the reference.
The name: system versus keyword
One point to fix immediately, because the repository uses the word two ways.
- Metacall (capitalized, the section name) is the system as a whole — the branded name for Logos metaprogramming, a sibling to Writ, Deem, and Trama.
metacall(lowercase, incodefont) is a specific keyword — the explicit compile-time-evaluation operator. It is one construct in the system, the one the system takes its name from, not the whole of it.
The umbrella term the compiler’s own docs use is metaprogramming / metafunctions. Whenever this documentation means the operator it says “the metacall keyword”; whenever it means the system it says “Metacall.” The distinction is load-bearing — do not blur them.
The three surfaces
Metacall exposes three shipping surfaces, all sitting on one compile-time JIT. They differ only in how the compiler is told to run your function and in what the function receives.
-
The
metacallkeyword — explicit compile-time evaluation (CTFE). Logos has no implicit const-eval;metacallis the replacement.let n: i64 = metacall add(2, 3);runsaddin the compiler and splices5. It has three expression forms (call, parenthesized-expr, block) and an item form. Reach for it when you want to force a value or a batch of items at a specific site. -
#[fn_macro]and#[token_macro]— thename!(...)invocation surface. A#[fn_macro]receives its arguments as parsed expression ASTs (ExprBlobvalues); a#[token_macro]receives the raw source bytes between the delimiters as astr, never parsed as Logos. This is how DSLs whose body is not valid Logos are embedded —deem!,wql!, andtrama!are all three-argument#[token_macro]s. -
#[metaprog_handler("trigger")]— derive-style hooks. A handler fires when the compiler scans a user item bearing a matching#[trigger]attribute; the handler synthesizes sibling items next to that target. This is how#[derive_clone]-style derives are written.
All three routes end in the same place: the metaprogram returns a typed AST fragment, and the compiler grafts it into the program.
The mental model: one JIT, quote and splice
Every surface follows the same three-beat cycle. The compiler synthesizes a no-argument thunk around your metaprogram, JIT-compiles it, invokes it, and replaces the original AST node with what the thunk returned. Your metaprogram does not build AST nodes by hand — it quotes them, using typed AST literals:
quote_item! { struct Synth { x: i32 } } // → QuoteItemBlob (an item)
quote_expr! { vec_from_arr([1, 2, 3]) } // → ExprBlob (an expression)
quote_ty! { Vec<i32> } // → a Type value
Inside a quote, antiquotation splices values bound in the surrounding metafunction — #(name) / #(blob) in quote_item!, bare #x in quote_expr!, $t in quote_ty! — and repeat groups like #( #elems ),* expand a cursor pack. The returned fragment (ExprBlob, QuoteItemBlob, or a Vec-of-items ItemList) is spliced in, and then re-enters the normal compiler: the spliced code is type-checked exactly as if you had written it by hand.
ASTs are Writ maps
The reason this is not a second language grafted onto the compiler is that a metaprogram’s inputs and outputs are ordinary Logos data. An ExprBlob, a QuoteItemBlob, an ItemList — each is a struct wrapping a serialized AST, and that AST is a Writ document: the same tagged-map format the runtime uses, the same bytes that go on the wire and on disk. The compiler’s own intermediate representation was migrated to be a shell over a Writ mirror, so the bytes are the IR. A metaprogram that reads or produces code consumes the same layout the compiler does — no impedance mismatch, no bespoke macro token type. Writ is the substrate Metacall’s fragments live in, which is why the two sections are so tightly coupled.
Monotonicity and the fixpoint
Metaprograms only ever add entities; they never mutate or delete existing ones. This monotonicity is what makes the cycle “AST → sema → run metaprograms → maybe new types → sema again” terminate cleanly: each discovery iteration either emits something new or the loop stops, bounded by a hard cap of 16 iterations. There is deliberately no AST-rewrite surface — synthesis produces new code, analysis is separate, and the two never cross.
Where Metacall sits
Metacall is foundational rather than a corner feature. The name!(...) macro surface — including the format family (println!, format!, …) and every DSL macro — is built on it. In particular, Logos’s own query and template DSLs are Metacall clients: deem! / wql! (see Deem) and trama! (see Trama) are #[token_macro]s that take a resource binding name, a raw parameter list, and a raw body, and emit a checked native pub fn at compile time. When you write a deem! query, you are using Metacall.
What ships, what’s designed
Be honest about the boundary, the way the Writ docs are about their unshipped corners.
Shipping today: the metacall keyword (all forms, with its return-type and staging restrictions enforced — there are failing tests for nested metacall, runtime-captured arguments, and non-primitive returns); #[fn_macro] / #[token_macro] at expression, item, and resource positions; #[metaprog_handler] derives; quote_item! / quote_expr! / quote_ty! with antiquotation and repeat groups; and gensym for hygiene.
Designed but not built: the full metafunction model of ADR 0003 — capability gating (ReflectCtx / InjectCtx / QueryCtx, IO/FFI forbidden), dependency-set scheduling, content-addressed incremental caching, typearg(T) reification, and implicit metacall in declaration positions. Also unbuilt: template body expansion (a template body is parsed and then silently dropped), the quote_stmt! / quote_pat! / quote_ident! forms, full def-site hygiene, and the transformative Pass<Rewrites, Diagnostics> phase. These are labeled clearly wherever they appear; treat them as roadmap, not API.
Related
- Metacall tutorial — build up from
metacall add(2, 3)through item generation, your first#[fn_macro]and#[token_macro], theresource = macro!(...){…}form, andgensym, all from real tests. - Metacall reference — every form of the
metacallkeyword, the macro signature tables, thequote_*!family and antiquotation,#[metaprog_handler], the splice/typing model, the fixpoint, hygiene, and a full status-and-gaps enumeration. - Writ: the data substrate — the tagged-map format that is Metacall’s AST representation; ASTs are Writ documents.
- Deem and Trama — the query and template DSLs, both built as
#[token_macro]s on this substrate.