Snipped and backdated from a Reddit comment of mine.
The biggest win for compile-time features is eliminating the need for clunky, external build tools when you need to generate boilerplate code. (See F#’s type providers)
Instead, you can write functions that run at compile-time to generate ASTs for you instead of mudging with text source code.
This feature in general is called compile-time function execution (CTFE).
The common scenario is that you have a pre-existing schema you want to generate code from (e.g. an SQL database or a web service using JSON, XML, Protobufs, or an overhyped four-letter acronym from the early-2000s).
CTFE solves lots of problems with “traditional” text-based code-gen:
Since you’re generating text instead of an actual AST, there is nothing stopping the tool from generating invalid source code the doesn’t compile. You end up working backward figuring out where you went wrong.
- …with CTFE, you’re working with the AST of the language (along the lines of Lisp macros that are executed at compile-time). Given a strongly typed language, invalid ASTs are compiler errors in the first place.
Generated source code can easily get out of sync with the source schema (e.g. forgot to regen the code after adding a column, etc.)
- …with CTFE, the schema can be read during development time to allow for code completion. Out of sync code becomes a compiler error, not a run-time error.
Controlling the source code generated generally is limited or non-existent, or requires dealing with extra configuration files (or some dynamically-loaded library reflection magic).
- …with CTFE, everything is just part of the code base; you can just pass a value to a function.
Other nice CTFE use cases:
- “Strongly typed” strings: compile JSON and XML strings into actual values, safe string interpolation
- Partial evaluation: pre-compute expressions ahead-of-time instead of at runtime (C++’s
- AST metaprogramming: generate types, functions bodies, etc.
- Anything else you might want to do at compile-time…
The major con with implementing CTFE is that introduces a great deal of complexity into the compiler implementation and needs to be well designed.
You end up with a dependency graph of compile-time and run-time code that depends on the output of a compile-time function (hopefully you have a keyword that distinguishes what functions you intend to CTFE).
Because of that, you have to figure the order in which you need to run each compile-time function before compiling the dependent, “regular” code that executes at run-time.
CTFE is going to run really slow unless you can run as much as possible in parallel.