A compiler is typically a binary to which we feed our source code (text) and get machine code (object files) as output.
But this means that the entire compilation pipeline (parsing, name resolution, type checking, desugaring, code generation) is opaque and the user can not modify it.
If we want to extend the surface language, we have to either create preprocessors or use metaprogramming (TH).
If we want to extend the optimizer, we need facilities such as rewrite rules.
No known way to extend scoping rules or code generation (in Haskell).
No control over the machine code that is produced.
While some of these points are addressed by having proper macros, it is the last one that led me to think in another direction.
Programming languages are an abstraction over machine code. In order to have perfect control over the machine code, we could write it directly, but that is difficult, tedious, and error prone. Our solution to this is a ‘compiler’ which hides the machine code from us, but we could instead expose it (assuming we don’t care about portability, which is at odds with performance anyway).
Take an assembly language, but augment it with a macro system. Provide all parts of a typical compiler pipeline (from parser to code generation) as a library of composable and extensible macros.
The macros themselves would be defined in a meta language, which would have built-in functionality for parsing context-free grammars, allocating unique names, doing AST transformations, doing unification and constraint solving, and whatever else a compiler needs.
And whenever you want to go low level and hand-write a piece of assembly, you just do.