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 defmacro is 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.

  • Schemesyntax-rules (R5RS+). Pattern templates with ... ellipsis.
  • Racketsyntax-rules, syntax-parse (rich pattern language with class definitions).
  • Rustmacro_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_macro crate (function-like, attribute, derive). Three flavors. Token-stream-based.
  • Scala (Scala 3) — quotes/splices: '{ ... } quote, $x splice. 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) — macro and elab macros; 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.
  • Elixirquote / unquote returns AST; defmacro defines 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 Lispdefmacro is non-hygienic. gensym for fresh names. Reader macros (#-dispatch macros) extend syntax itself.
  • Schemesyntax-case (R6RS) is procedural but hygienic; older Scheme had non-hygienic defmacro extensions.
  • 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.

  • Zigcomptime. 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++23 if consteval.
  • Rustconst 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 where clauses; #assert at 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 T at runtime.
  • [[Languages/csharp|C#]] — generics with reified types; typeof(T) works at runtime.
  • Kotlin — JVM generics; inline + reified to 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.

  • Javajava.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.
  • Rubymethod_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”.
  • GroovyExpandoMetaClass; categories; per-instance metaclass.
  • Perl — symbol table manipulation (*foo = ...); Moose MOP via Class::MOP.
  • SwiftMirror for value reflection (limited).
  • Dartdart:mirrors (caveats: not in AOT/Flutter; deprecated).
  • Luadebug library + metatables = practical reflection.
  • R — environments are first-class; quote/bquote/substitute for NSE.
  • Prologassert/retract modify the database; term_to_atom for serialization.
  • Erlangapply, parse transforms, hot code reloading.
  • ElixirCode, 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.
  • Dartbuild_runner + source_gen. Used heavily for JSON, freezed, riverpod codegen.
  • Rustbuild.rs runs arbitrary Rust at build time, emits .rs files.
  • 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 Lispset-macro-character, *read-table* manipulation. Add new literal syntax.
  • Racket#lang directive: 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)

  • COBOLCOPY / REPLACE (preprocessor only).
  • SQL — none in the language; dialects have CREATE FUNCTION/TRIGGER but not source-manipulating.
  • Basheval, source manipulation; rudimentary.
  • Go (pre-1.18) — go generate for 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.
  • Tcleval + uplevel/upvar give runtime metaprogramming via string substitution; no AST macros.
  • Ada — generics are structural metaprogramming; no AST manipulation.

Per-language quick reference

LanguagePrimary mechanismHygienePhase
PythonDecorators + reflectionn/a (runtime)runtime + import time
JavaScriptDecorators + Proxy/Reflectn/aruntime
TypeScriptDecorators + type-level computationn/a / type-onlycompile (types) + runtime (decorators)
JavaAnnotations + APT + reflectionhygienic (codegen)compile (APT) + runtime (refl)
CC preprocessornon-hygienic (text)preprocess
C++Templates + constexpr + conceptshygieniccompile
[[Languages/csharp|C#]]Source generators + attributes + reflectionhygienic (SG)compile (SG) + runtime (refl)
Gogo generate (external); no in-lang macrosn/aexternal
Rustmacro_rules! + proc macros + build.rshygieniccompile
SwiftMacros (5.9+) + property wrappers + result buildershygieniccompile
KotlinKSP + annotations + reflectionhygienic (KSP)compile + runtime
Rubymethod_missing + define_method + open classesn/a (runtime)runtime
PHPAttributes (8+) + reflectionn/acompile (attr) + runtime
ScalaQuotes/splices (3) or def macros (2) + match typeshygieniccompile
Dartbuild_runner + macros (preview) + dart:mirrorshygieniccompile + runtime
Elixirquote/unquote + defmacrohygieniccompile
HaskellTemplate Haskell + type families + RULEShygieniccompile (TH) + type-check
Clojuresyntax-quote + defmacrohygienic (auto-gensym)compile (read)
[[Languages/fsharp|F#]]Type providers + computation expressions + quotationshygieniccompile + runtime
LuaMetatables + debug libraryn/a (runtime)runtime
RNSE (substitute/quote/bquote) + S4/R6 + tidy evaln/aruntime
JuliaMacros + generated functions + @code_*hygienic (auto)compile (macro) + runtime
Zigcomptimehygieniccompile
NimTemplates + macros + term-rewriting + CTFEhygieniccompile
CrystalMacros (Crystal-syntax AST manipulation)hygieniccompile
OCamlPPX rewriters + functors + first-class moduleshygieniccompile
PerlSource filters + symbol table + Moose MOPnon-hygieniccompile (filter) + runtime
ErlangParse transforms + hot reload + applyhygienic (parse-trans)compile + runtime
Racketsyntax-parse + lang + reader extensionshygieniccompile (read)
Common Lispdefmacro + reader macros + MOPnon-hygieniccompile (macro) + runtime (MOP)
Schemesyntax-rules (R5+) / syntax-case (R6+)hygieniccompile
FortranC preprocessor (fpp)non-hygienicpreprocess
COBOLCOPY/REPLACE preprocessornon-hygienicpreprocess
AdaGenerics (structural)hygieniccompile
PascalGenerics + RTTIhygieniccompile + runtime
Prologassert/retract + term reflectionn/aruntime
Tcleval + uplevel/upvar (string-based)non-hygienicruntime
GroovyAST transforms (@-) + ExpandoMetaClasshygienic (AST)compile (AST) + runtime
Bashevalnon-hygienicruntime
PowerShellAST manipulation + ScriptBlock + .NET reflectionhygieniccompile + runtime
SQLnone in the languagen/an/a
V$if/$for/$compile_errorhygieniccompile
Odinparametric procs + where + #asserthygieniccompile
RocAbilities + opaque typeshygieniccompile (type-level)
Gleamnone (intentional)n/an/a
Ponynone (intentional)n/an/a
IdrisElaborator reflection + dependent typeshygieniccompile
Leanmacro + elab framework + tacticshygieniccompile
Coq (Rocq)Ltac + Ltac2 + MetaCoq + Notation systemhygieniccompile
AgdaReflection (unquoteDecl, quoteTerm) + instance argshygieniccompile
SmalltalkFull reflective MOP + image manipulationn/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 $ident capture 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 ast module 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