Some ideas how to evolve Nim's macro system further:
https://github.com/nim-lang/RFCs/issues/212
https://github.com/nim-lang/RFCs/issues/212#issuecomment-738247061
@mratsim
Is Continuation-Passing Style shelved or deemed to complex for v1 and will be revisited later?
Honestly , CPS seems unavoidable to me (at the long run), I'd implement it together with a derivative of lambda-mu calculus.
@Araq
I still don't understand continuations for macros. :-)
don't believe it.
Macros compose already, they compose with the compiler. The continuation of a macro is the compiler itself. A continuation can be written as a pair (Upd(A),E) where E stands for the environment/the evaluation context and Upd stands for an update function - updating the evaluation context with the input A. Let's mark an untyped A (e.g. an untyped fragment of the AST) with a "?". Therefore, an untyped Macro can be written as (Upd(?A),?E) that receives an untyped AST fragment, integrates it in its own (untyped) context and passes the result, the updated evaluation context, to the compiler. But macros can't receive input from other macros. Moreover, they can't receive input from macros that are parameters themselves. How could this look like ? (macros implicitly untyped here)
proc aproc(gmacro : aliassym macro, ...) =
#... insert your statements here
localmacro:
# ... insert your statements here
gmacro:
# ... insert your statements here
a proc would receive a macro as a parameter , then gmacro would be called by the compiler with its own AST input, finally, a macro localmacro would receive an untyped AST that contains the result of gmacro . We would end with localmacro(gmacro(?A)) that stands for (Upd(?gmacro(?A)),?E) and this is certainly a composition of two macros. (?E being the evaluation context added by the outer macro localmacro.)
This is not implemented in current Nim. If implemented, the parser had to redirect the output of the inner macro into the outer macro. I can't judge if this might be a desirable feature. Perhaps macros should in general be regarded as a very limited instrument for processing local AST fragments. In fact, a kind of AST Assembler that remains to stay.
If so, we could look for something else. E.g. a defunctionalized version of CPS-transformed programs with a high-level AST, allowing for extremely fine-grained evaluation contexts. working for functional and imperative languages as well. Example:
https://ix.cs.uoregon.edu/~pdownen/publications/scfp.pdf
In this particular case, the compiler would reify the source code as a composition of macros. They could be made accessible to the programmer and therefore allow for specific composition of code blocks or code generators.
However, this might be in the distant future for Nim. Actually, there is an RFC on github (timotheecour's proposal) that can give us some enlightment , if and how Nim's macros can be developed further.
Macros compose already, they compose with the compiler.
They don't compose well. ;-) However, the primary reason is the over-reliance on untyped: Every macro that needs to inspect the AST and uses untyped does not see expanded templates. My solution is to make the typed AST well specified (currently only the untyped AST is specified) and to provide mechanisms to describe potential symbol injections. One use case after another should work with typed, untyped should be a last resort mechanism.
... But still, the AST needs to be rerun by the compiler itself.
Yes, sure. Not a problem.
=> conclusion: AST inspection is dead then.
No, that doesn't follow. But only because I forgot to mention a crucial detail. The compiler does a "best effort" analysis for typed ASTs and potential errors remain in the AST structure. For example:
macro async(t: typed): untyped =
# ...
# t is nkCall(ident"await", nkCall(sym"f", 1, 2, 3))
proc f(a, b, c: int): Future[int] = ...
proc p() {.async.} =
await f(1, 2, 3)
Notice how "await" wasn't resolved to a symbol but "f" was.