Metaprogramming is the practice of writing programs that themselves write other programs. For example, the GNU Autoconf tool produces configuration scripts for software installation.
The origins of metaprogramming are as old as programming itself, and are thus lost in the mists of time. However, most people would argue that metaprogramming as we know it today probably originates somewhere between the brackets of LISP.
Metaprograms are programs which manipulate code instead of data. More precisely, metaprograms manipulate data that represents code. Fundamentally, a metaprogramming system needs a way to transform data into code and vice versa. These two operations are called "splicing" and "quoting" respectively. A metaprogram will take quoted code as an input and produce quoted code as an output. The output will then be spliced into a program.
Beyond this, metaprogramming systems vary wildly. There are a very large amount of axes one can move along in the design of their system, I will cover a few below.
Note: The following is relevant only for statically-typed languages.
One consideration when designing a metaprogramming system is whether metaprograms can generate ill-typed code. This is a concern because ill-typed metaprogramming can cause violation of abstraction boundaries. Say that we have a library metaprogram generates ill-typed code. When a programmer uses this metaprogram, type errors will appear originating from the library. This forces the user to look at the library code to figure out what went wrong, which breaks the abstraction it is supposed to provide.
The same can be said for scoping. When designing a metaprogramming system, it is important to consider whether metaprograms can generate code with unbound variables.
Quoting transforms code into data, but what does this data look like? It is a token stream? An AST? A low-level IR? This question depends on what your metaprogramming is meant for. If it is meant for implementing custom syntax, a token stream may be more appropriate. If it is meant for domain-specific program optimization, a low-level IR may be the best choice. So on and so forth for other domains.
Another consideration is when metaprograms execute. In many systems metaprograms are restricted to compile-time execution, but in others metaprograms are allowed to execute at runtime. Allowing this requires programs to carry around the compiler at runtime. This is less of a problem for interpreted or JIT compiled languages.
A partial evaluator executes a program on all its compile-time known inputs. This is called specialization. Depending on how it is implemented, partial evaluation (PE) can subsume many optimizations such as inlining, monomorphization, and constant folding.
PE can be unreliable. Determining what parts of the program will be specialialized - and coaxing them into specialization when they are not - can be very difficult. Multistage programming is a variant of PE that uses explicit annotations to direct specialization. Thus, the uncertainty of automatic PE is removed.
MetaML, a staged programming language.
András Kovács's Staged Compilation with Two-Level Type Theory.
Materials for course on metaprogramming at the University of Cambridge (2018-2019).
Some information about the International Summer School on Metaprogramming that took place at Robinson College, Cambridge in 2016.