Metaprogramming — Cross-Language Comparison
Metaprogramming — Cross-Language Comparison
- Type: cross-cutting comparison
- Languages covered: 51 (see _index)
- Last updated: 2026-05-07
TL;DR
- Metaprogramming = code that manipulates code. Comes in five flavors: macros (manipulate AST), reflection (inspect/modify at runtime), codegen (compile-time generation), templates (parametric code expansion), and type-level computation (types that compute).
- The Lisp tradition (homoiconic + macros) is still the gold standard for power. Modern languages reach for it via different mechanisms — Rust proc macros, Scala 3 quotes, Swift macros (5.9), Lean 4 elab.
- Hygienic vs non-hygienic is the big macro divide. Hygiene = your macro doesn’t accidentally capture user-side variables. Modern macros are hygienic; CL
defmacrois famously not. comptime(Zig) is the simplest metaprogramming model in any modern language: regular code that runs at compile time. No separate macro language to learn.- Smalltalk has the most reflective runtime of any language — you can rewrite the compiler from inside the running image. Common Lisp (with MOP) is the only close peer.
- Type-level computation in Haskell/TypeScript/Scala/Rust/Idris/Agda/Lean turns the type checker into a programming environment. Powerful but inscrutable.
The spectrum
NONE ──── REFLECTION ──── DECORATORS ──── TEMPLATES ──── HYGIENIC MACROS ──── PROC MACROS ──── DEPENDENT TYPES
| | | | | | |
COBOL Java refl Python @ C++ tmpl Scheme syn-rules Rust proc Idris
SQL C# refl JS decorators D tmpl Racket syn-parse Scala 3 Agda
Bash Smalltalk MOP Java APT Rust generics Clojure syn-quote Swift macros Lean
Pony Common Lisp MOP TS decorators Java generics Elixir quote OCaml ppxlib Coq/Rocq
Go (pre-gen) Ruby method_ C# attr+SG Concepts Julia macros Lean 4 elab
missing Nim macros
CL defmacro (non-hyg)
Categories
1. Hygienic pattern-based macros (syntax-rules)
Macros declared by patterns; compiler ensures no accidental capture. Limited expressiveness vs procedural macros.
- Scheme —
syntax-rules(R5RS+). Pattern templates with...ellipsis. - Racket —
syntax-rules,syntax-parse(rich pattern language with class definitions). - Rust —
macro_rules!(declarative macros). Pattern + template; hygienic by default. - Julia — macros are procedural BUT auto-hygienic via gensym;
esc()to escape.
2. Hygienic procedural macros (typed AST manipulation)
Macros are functions from AST → AST. Hygiene is the default; you can write any code-walking transform.
- Rust — proc macros via
proc_macrocrate (function-like, attribute, derive). Three flavors. Token-stream-based. - Scala (Scala 3) — quotes/splices:
'{ ... }quote,$xsplice. Typed AST. Macros are inline methods that compute at compile time. - Swift — Swift macros (5.9+): expression macros (
#myMacro), declaration macros (@MyMacro), implemented as Swift programs. - Lean (4) —
macroandelabmacros; full hygienic system using elaborator combinators. Possibly the most expressive macro system in any current language. - OCaml — PPX (preprocessor extensions) via ppxlib. Typed AST. Used for serialization, deriving, language extensions.
- Elixir —
quote/unquotereturns AST;defmacrodefines macros. Hygienic by default;var!to escape. - Clojure — syntax-quote
`(...)+~/~@unquote. Auto-gensym (x#) for hygiene;~'to escape. - Agda — reflection API:
unquoteDecl,Term,quoteTerm. Most expressive of the dependent-typed languages. - Coq (Rocq) — Ltac (the tactic language) is a kind of metaprogramming. MetaCoq formalizes Coq’s reflection in Coq itself.
- Idris (2) — elaborator reflection.
3. Non-hygienic macros (the original Lisp model)
Direct AST manipulation; capture is up to the macro author. Maximum power, maximum footgun.
- Common Lisp —
defmacrois non-hygienic.gensymfor fresh names. Reader macros (#-dispatch macros) extend syntax itself. - Scheme —
syntax-case(R6RS) is procedural but hygienic; older Scheme had non-hygienicdefmacroextensions. - Perl — source filters (
Filter::Util::Call) — non-hygienic raw text manipulation. PPI for parse-tree access.
4. Compile-time function execution (CTFE / comptime)
Regular language code runs at compile time. No separate macro language.
- Zig —
comptime. The language’s primary metaprogramming tool. Generic types are functions returning types at compile time. - Nim — full CTFE;
static[T]parameters; macros + templates + term-rewriting macros all coexist. - D — (mention) CTFE since 2007; static if; mixin templates.
- C++ —
constexpr/consteval/constinit; templates + concepts (C++20). C++23if consteval. - Rust —
const fn(limited but growing); proc macros for unrestricted compile-time code. - Crystal — macros are compile-time AST manipulation in Crystal-like syntax.
- V —
$if,$for,$compile_error— limited comptime DSL. - Odin — parametric procedures with
whereclauses;#assertat compile time.
5. Templates / parametric code expansion
Generic types/functions that the compiler instantiates per type. Templates can be Turing-complete (C++).
- C++ — templates + SFINAE → concepts. Turing-complete metaprogramming.
- Rust — generics are templates with constraints (
where T: Bound); coherence enforced. - Java — generics with erasure; can’t see
Tat runtime. - [[Languages/csharp|C#]] — generics with reified types;
typeof(T)works at runtime. - Kotlin — JVM generics;
inline+reifiedto recover type info. - Swift — generics + protocols + opaque types.
- Scala — generics + higher-kinded types + match types (Scala 3 — types as pattern matches).
- D — templates + mixin templates + static if + alias templates.
- Nim — templates (hygienic-ish substitution) + macros (AST manipulation).
- Ada — generic packages (parameterized modules).
- OCaml — functors (parameterized modules — the original “modules with parameters”).
- SML — (mention) the original ML functor.
6. Decorators / annotations / attributes
Syntactic sugar to wrap or annotate functions/classes; reflectively inspectable at runtime or processed at compile time.
- Python —
@decorator. Functions wrapping functions/classes. Pure Python; runtime. - JavaScript — TC39 decorators (Stage 3 → close to standard).
- TypeScript — decorators (legacy + new TC39 style).
- Java — annotations + APT (Annotation Processing Tool). JSR-269.
- [[Languages/csharp|C#]] — attributes; source generators (Roslyn) read attributes at compile time.
- Scala — annotations + macros.
- Groovy — AST transformations via
@-annotations (@Immutable,@CompileStatic). - Kotlin — annotations + KSP (Kotlin Symbol Processing).
- Dart — annotations +
build_runner+source_gen. - Swift — property wrappers + result builders + macros (5.9+).
- Rust — attributes (
#[derive(...)],#[cfg(...)]); attribute macros are proc macros.
7. Reflection / introspection at runtime
The runtime exposes program structure (classes, methods, fields) for query/modification.
- Java —
java.lang.reflect; method handles; class loaders; instrumentation agents. - [[Languages/csharp|C#]] / [[Languages/fsharp|F#]] —
System.Reflection; emit IL at runtime; full visibility. - Python —
__dunders__,inspect,dir. Everything is reflectable. - Ruby —
method_missing,define_method,class_eval. Open classes. The MOP via Class. - Smalltalk — total reflective access.
become:swaps object identities atomically.doesNotUnderstand:for proxies. - Common Lisp — CLOS + MOP (Metaobject Protocol). The original “rewrite the compiler from inside the language”.
- Groovy —
ExpandoMetaClass; categories; per-instance metaclass. - Perl — symbol table manipulation (
*foo = ...); Moose MOP via Class::MOP. - Swift —
Mirrorfor value reflection (limited). - Dart —
dart:mirrors(caveats: not in AOT/Flutter; deprecated). - Lua —
debuglibrary + metatables = practical reflection. - R — environments are first-class;
quote/bquote/substitutefor NSE. - Prolog —
assert/retractmodify the database;term_to_atomfor serialization. - Erlang —
apply, parse transforms, hot code reloading. - Elixir —
Code,Module, AST inspection at runtime.
8. Source generation / build-time codegen
Read source/annotations, write more source. Compile-time.
- [[Languages/csharp|C#]] — Roslyn source generators. Read syntax tree; emit code into compilation.
- Kotlin — KSP (Kotlin Symbol Processing). Successor to KAPT.
- Dart —
build_runner+source_gen. Used heavily for JSON, freezed, riverpod codegen. - Rust —
build.rsruns arbitrary Rust at build time, emits.rsfiles. - Swift — macros (5.9+) overlap heavily with this; pre-5.9, sourcery (third-party).
- Java — APT + Lombok (controversial but ubiquitous).
- Scala — sbt source generators; macros.
- Groovy — AST transformations.
- OCaml — PPX rewriters + dune.
9. Image-time / live metaprogramming
Modify the running system, not source files. Smalltalk’s headline feature.
- Smalltalk (Pharo, Squeak) — recompile any class from inside the running image. The IDE is written in Smalltalk and is part of the image.
- Common Lisp — connect to a running Lisp via SLIME/Sly; redefine functions; new behavior takes effect immediately.
10. Type-level computation (compute on types at the type-checker)
Types compute. The type checker becomes a programming environment.
- Haskell — type families, GADTs, DataKinds, kind polymorphism. Singletons (the singletons library) lifts values into types.
- TypeScript — conditional types (
T extends U ? X : Y), mapped types, template literal types,infer, variadic tuples. Famously Turing-complete. - Scala (Scala 3) — match types (types defined by pattern match), opaque types, type lambdas.
- Rust — associated types + traits + GATs; const generics (numeric type params).
- Swift — primary associated types; opaque return types (
some P); existential types (any P). - Idris / Agda / Lean / Coq (Rocq) — full dependent types: types ARE values.
11. Reader macros / syntax extension
Macros that change how the parser reads source text.
- Common Lisp —
set-macro-character,*read-table*manipulation. Add new literal syntax. - Racket —
#langdirective: each file declares its own language. The basis of “language-oriented programming”. - Perl — source filters; Devel::Declare (legacy).
12. Term rewriting / expression simplification
Compile-time pattern-based code rewriting.
- Nim — term-rewriting macros (
{.tags: [...].}patterns). - Maude — (mention) the canonical term-rewriting language.
13. No metaprogramming (or extremely limited)
- COBOL —
COPY/REPLACE(preprocessor only). - SQL — none in the language; dialects have CREATE FUNCTION/TRIGGER but not source-manipulating.
- Bash —
eval, source manipulation; rudimentary. - Go (pre-1.18) —
go generatefor codegen externally; no in-language metaprogramming. Generics added 1.18 but no macros. - Pony — intentionally no macros.
- Gleam — intentionally no macros.
- Fortran — preprocessor (
fpp/cpp); no first-class metaprogramming. - Tcl —
eval+ uplevel/upvar give runtime metaprogramming via string substitution; no AST macros. - Ada — generics are structural metaprogramming; no AST manipulation.
Per-language quick reference
| Language | Primary mechanism | Hygiene | Phase |
|---|---|---|---|
| Python | Decorators + reflection | n/a (runtime) | runtime + import time |
| JavaScript | Decorators + Proxy/Reflect | n/a | runtime |
| TypeScript | Decorators + type-level computation | n/a / type-only | compile (types) + runtime (decorators) |
| Java | Annotations + APT + reflection | hygienic (codegen) | compile (APT) + runtime (refl) |
| C | C preprocessor | non-hygienic (text) | preprocess |
| C++ | Templates + constexpr + concepts | hygienic | compile |
| [[Languages/csharp|C#]] | Source generators + attributes + reflection | hygienic (SG) | compile (SG) + runtime (refl) |
| Go | go generate (external); no in-lang macros | n/a | external |
| Rust | macro_rules! + proc macros + build.rs | hygienic | compile |
| Swift | Macros (5.9+) + property wrappers + result builders | hygienic | compile |
| Kotlin | KSP + annotations + reflection | hygienic (KSP) | compile + runtime |
| Ruby | method_missing + define_method + open classes | n/a (runtime) | runtime |
| PHP | Attributes (8+) + reflection | n/a | compile (attr) + runtime |
| Scala | Quotes/splices (3) or def macros (2) + match types | hygienic | compile |
| Dart | build_runner + macros (preview) + dart:mirrors | hygienic | compile + runtime |
| Elixir | quote/unquote + defmacro | hygienic | compile |
| Haskell | Template Haskell + type families + RULES | hygienic | compile (TH) + type-check |
| Clojure | syntax-quote + defmacro | hygienic (auto-gensym) | compile (read) |
| [[Languages/fsharp|F#]] | Type providers + computation expressions + quotations | hygienic | compile + runtime |
| Lua | Metatables + debug library | n/a (runtime) | runtime |
| R | NSE (substitute/quote/bquote) + S4/R6 + tidy eval | n/a | runtime |
| Julia | Macros + generated functions + @code_* | hygienic (auto) | compile (macro) + runtime |
| Zig | comptime | hygienic | compile |
| Nim | Templates + macros + term-rewriting + CTFE | hygienic | compile |
| Crystal | Macros (Crystal-syntax AST manipulation) | hygienic | compile |
| OCaml | PPX rewriters + functors + first-class modules | hygienic | compile |
| Perl | Source filters + symbol table + Moose MOP | non-hygienic | compile (filter) + runtime |
| Erlang | Parse transforms + hot reload + apply | hygienic (parse-trans) | compile + runtime |
| Racket | syntax-parse + lang + reader extensions | hygienic | compile (read) |
| Common Lisp | defmacro + reader macros + MOP | non-hygienic | compile (macro) + runtime (MOP) |
| Scheme | syntax-rules (R5+) / syntax-case (R6+) | hygienic | compile |
| Fortran | C preprocessor (fpp) | non-hygienic | preprocess |
| COBOL | COPY/REPLACE preprocessor | non-hygienic | preprocess |
| Ada | Generics (structural) | hygienic | compile |
| Pascal | Generics + RTTI | hygienic | compile + runtime |
| Prolog | assert/retract + term reflection | n/a | runtime |
| Tcl | eval + uplevel/upvar (string-based) | non-hygienic | runtime |
| Groovy | AST transforms (@-) + ExpandoMetaClass | hygienic (AST) | compile (AST) + runtime |
| Bash | eval | non-hygienic | runtime |
| PowerShell | AST manipulation + ScriptBlock + .NET reflection | hygienic | compile + runtime |
| SQL | none in the language | n/a | n/a |
| V | $if/$for/$compile_error | hygienic | compile |
| Odin | parametric procs + where + #assert | hygienic | compile |
| Roc | Abilities + opaque types | hygienic | compile (type-level) |
| Gleam | none (intentional) | n/a | n/a |
| Pony | none (intentional) | n/a | n/a |
| Idris | Elaborator reflection + dependent types | hygienic | compile |
| Lean | macro + elab framework + tactics | hygienic | compile |
| Coq (Rocq) | Ltac + Ltac2 + MetaCoq + Notation system | hygienic | compile |
| Agda | Reflection (unquoteDecl, quoteTerm) + instance args | hygienic | compile |
| Smalltalk | Full reflective MOP + image manipulation | n/a (runtime) | runtime |
Decision rubric
Pick non-hygienic macros (CL defmacro) when you want maximum power and accept the discipline. Lisp dialects only.
Pick hygienic macros (Scheme syntax-rules, Rust macro_rules!) when you want reliable substitution without surprise. Most modern languages.
Pick proc macros (Rust, Scala 3, Swift, Lean) when patterns aren’t enough — you need to walk the AST.
Pick comptime (Zig) when macros feel like a separate language. comptime IS the language.
Pick decorators / annotations when you want declarative attachment of behavior. Python, Java, C#, Kotlin.
Pick source generators (C# SG, Kotlin KSP, Dart build_runner) when the codegen has runtime cost in the alternatives. SG/KSP read the syntax tree at compile time.
Pick reflection when generation must happen at runtime (plugin loading, ORM mappings discovered late). Java, .NET, Python, Ruby.
Pick image-based metaprogramming when “live” matters — debugging, exploratory work, simulation. Smalltalk, Common Lisp.
Pick type-level computation when the cost of runtime checks is unacceptable AND your team can read it. Haskell, TypeScript (with care), Scala 3, Idris/Agda/Lean.
Edge cases & nuances
- Hygienic doesn’t mean intuitive — Rust’s
macro_rules!hygiene rules around$identcapture surprised many. Scala 3 quotes are mostly hygienic but break in subtle ways across phase boundaries. - TypeScript types are Turing-complete — there’s a regex-engine implemented entirely in the type system. Compile times are the cost; ts-go (TS 7) helps.
- Lisp macros are about reading code, not writing macros — the value is the invariant that source = data. Python’s
astmodule is similar but you build the tree manually; Lisp gets it for free. - Smalltalk’s
become:atomically swaps the identity of two objects. Used to be the canonical way to upgrade a class’s instances after a class redefinition. No other language has this. - MetaCoq formalizes Coq’s reflection in Coq itself — you can write tactics that are proved correct against the kernel.
- Swift macros (5.9+) are run as Swift programs — they execute outside the compiler in a separate process. Slower than Rust proc macros but safer (sandboxed).
- Lean 4’s elaboration framework is the most expressive macro system in any current language: macros AND elaboration AND tactics are all in the same language using the same primitives.
- Crystal macros are written in Crystal, run at compile time, manipulate Crystal AST. Maybe the cleanest macro system of any modern language: same syntax for compile-time and runtime.
- Dart macros are still in preview — Dart team is iterating on the design.
- PowerShell’s AST API lets you manipulate parsed PowerShell from PowerShell —
[System.Management.Automation.Language.Parser]::ParseInput(...).
Cross-references
For per-language detail, see each note’s God mode section — the metaprogramming hooks are deliberately the centerpiece. The 51 individual notes are linked from _index.
Citations
- Hoyte, Let Over Lambda — the canonical Common Lisp macros book.
- Brindle, “Writing Hygienic Macros” — Racket idioms.
- On Lisp by Paul Graham — classic.
- Rust proc macro reference: https://doc.rust-lang.org/reference/procedural-macros.html
- Scala 3 metaprogramming: https://docs.scala-lang.org/scala3/reference/metaprogramming/
- Swift macros (SE-0382): https://github.com/apple/swift-evolution/blob/main/proposals/0382-expression-macros.md
- Lean 4 metaprogramming: https://leanprover.github.io/theorem_proving_in_lean4/
- TypeScript Handbook (Type Manipulation): https://www.typescriptlang.org/docs/handbook/2/types-from-types.html
- The 51 per-language notes in _index are the underlying source.