3 min read
On this page

Metaprogramming

Metaprogramming is writing programs that manipulate programs — generating, analyzing, or transforming code at compile time or runtime.

Reflection

Reflection allows a program to inspect and modify its own structure at runtime.

Introspection

Examine type information, fields, methods at runtime.

// Limited reflection via type inspection
FUNCTION PRINT_TYPE(val)
    IF TYPE_OF(val) = Integer
        PRINT "It's an integer"

Full reflection (Java, C#, Python): Access field names, method signatures, annotations at runtime. Create objects dynamically. Invoke methods by name.

# Python reflection
class Foo:
    x = 42
    def hello(self): return "world"

obj = Foo()
print(getattr(obj, 'x'))           # 42
print(hasattr(obj, 'hello'))       # True
method = getattr(obj, 'hello')
print(method())                     # "world"
print(type(obj).__name__)           # "Foo"

Trade-offs: Powerful but bypasses type safety. Hard to optimize (JIT can help). Can break encapsulation.

Macros

Code that generates code at compile time.

Textual Macros (C Preprocessor)

Simple text substitution. No understanding of language structure.

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

// Bug: SQUARE(i++) expands to ((i++) * (i++)) — double increment!

Problems: No hygiene (name collisions), no type safety, surprising behavior with side effects, hard to debug.

Syntactic Macros (Lisp)

Operate on the AST (abstract syntax tree). Code and data have the same representation (homoiconicity).

(defmacro when (condition &body body)
  `(if ,condition (progn ,@body)))

;; (when (> x 0) (print x) (print "positive"))
;; expands to:
;; (if (> x 0) (progn (print x) (print "positive")))

Homoiconicity: Code is data (lists). Macros receive code as data structures, transform them, and return new code.

Hygienic Macros (Scheme, Rust)

Prevent accidental name capture. Variables introduced by the macro don't conflict with user variables.

// Hygienic macro: generates code at compile time
MACRO MY_VEC(elements...)
    temp_vec ← empty list
    FOR EACH x IN elements
        APPEND x TO temp_vec
    RETURN temp_vec

v ← MY_VEC(1, 2, 3)   // expands safely — temp_vec doesn't clash

Rust's macro_rules! is hygienic by default.

Procedural Macros (Rust)

Full Rust code that operates on token streams at compile time.

// Derive macro (auto-generates interface implementations)
@DERIVE(Debug, Clone, Serialize, Deserialize)
RECORD User
    name: string
    age: integer

// Attribute macro
@ASYNC_MAIN
ASYNC PROCEDURE MAIN()   // ...

// Function-like macro (compile-time SQL validation)
sql ← QUERY!("SELECT * FROM users WHERE id = $1", user_id)

Procedural macros can: Parse Rust syntax, generate arbitrary code, perform compile-time validation, implement custom derive traits.

Used extensively: serde (serialization), tokio (async runtime), diesel (database ORM), clap (CLI parsing).

Templates and Generics

C++ Templates

Turing-complete compile-time computation.

template<int N>
struct Factorial {
    static const int value = N * Factorial<N-1>::value;
};
template<>
struct Factorial<0> {
    static const int value = 1;
};

// Factorial<5>::value == 120 (computed at compile time)

Template metaprogramming: Compute types and values at compile time. Powerful but with terrible error messages and slow compilation.

Rust Generics

Monomorphized at compile time (like C++ templates but with better error messages via trait bounds).

FUNCTION LARGEST<T: Comparable>(list)
    max ← list[0]
    FOR EACH item IN list[1..]
        IF item > max THEN max ← item
    RETURN max
// Compiler generates separate versions for each concrete type

Const Generics

Parameterize types by values (not just types).

// Parameterize types by values (const generics)
CLASS Matrix<ROWS: integer, COLS: integer>
    FIELDS: data[ROWS][COLS]

// Matrix multiplication with compile-time dimension checking
FUNCTION MULTIPLY(Matrix<R,C>, Matrix<C,K>) -> Matrix<R,K>
    // ...implementation...
// Dimension mismatch caught at compile time!

Code Generation

Source Code Generation

Programs that output source code.

Examples: Protocol Buffers (protoc generates code from .proto), OpenAPI generators, ORM code generators (SQLAlchemy, Diesel).

Build script code generation (Rust build.rs): Generate Rust code at build time from schemas, grammars, or other inputs.

Annotation Processing (Java)

Process annotations at compile time to generate additional source files.

@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
}
// Annotation processor generates DAO, builder, etc.

Compile-Time Computation

const fn (Rust): Functions evaluated at compile time.

// Compile-time function evaluation
CONST FUNCTION FIBONACCI(n)
    IF n = 0 THEN RETURN 0
    IF n = 1 THEN RETURN 1
    RETURN FIBONACCI(n - 1) + FIBONACCI(n - 2)

CONST FIB_10 ← FIBONACCI(10)   // computed at compile time = 55

constexpr (C++): Similar concept.

Zig comptime: The entire language is available at compile time. No separate macro language.

DSL Embedding

Embed a Domain-Specific Language within a general-purpose language.

Internal DSLs

Use the host language's syntax to create a DSL-like API.

// Builder pattern as internal DSL
query ← Query.NEW()
    .SELECT(["name", "age"])
    .FROM("users")
    .WHERE("age > 18")
    .ORDER_BY("name")
    .LIMIT(10)
    .BUILD()

External DSLs

Separate language with its own parser.

SELECT name, age FROM users WHERE age > 18 ORDER BY name LIMIT 10;

Macros for DSLs

// HTML macro (DSL embedded via macros)
HTML_MACRO
    <div class="container">
        <h1>"Hello World"</h1>
        <p>"Count: " + count</p>
        <button onclick=increment>"+1"</button>
    </div>

Staging

Multi-stage programming: Generate and execute code in stages.

Stage 0: Write the code generator. Stage 1: Run the generator, producing optimized code. Stage 2: Run the generated code on actual data.

Example: A matrix multiplication library generates specialized code for specific matrix sizes at compile time. The generated code has no loops or conditionals — just straight-line arithmetic.

MetaOCaml: A language with explicit staging annotations (.<code>., .~(splice)).

Applications in CS

  • Serialization frameworks: serde (Rust), Jackson (Java) use macros/reflection to generate serialization code.
  • Web frameworks: Routing macros, ORM code generation, template engines.
  • Testing: Auto-generated test cases, mock generation, property-based test shrinking.
  • Build systems: Generate code from schemas (protobuf, flatbuffers, Cap'n Proto).
  • Compilers: Compiler-compilers (yacc, ANTLR) are metaprograms. JIT compilers generate code at runtime.
  • Game engines: Script binding generation, shader compilation, entity component system macros.
  • Scientific computing: Expression templates (C++) avoid temporary allocations. Auto-differentiation via code transformation.