Once a reliable baseline is available for Qi's performance, we will want to write a Qi-specific compiler that operates on the generated Qi code in the expansion step, to produce optimized Racket expansions.
Overview
Qi will soon have first class macro-extensibility, which means that Qi macros written by users would be indistinguishable from built-in Qi forms, allowing extension in arbitrary ways. At that stage, many of Qi's current "core" forms could themselves be implemented internally as such macros, which would expand into a small core language yet to be distilled.
In tandem with that effort, we also will want to write a compiler that will enter the lifecycle after the macro expansion phase and expand the resulting Qi forms into optimized Racket code for improved performance.
Details
Define Qi* to be the full Qi language including both the core forms as well as any macros, and let Qi0 be a "core" Qi language, representing the end state of Qi macro expansion. Then, the full lifecycle of a Qi expression before it is evaluated is:
Each individual macro expansion rule is a syntax transformation Qi* → Qi*. The macro expansion phase as a whole (i.e. with repeated application of transformation rules) is a transformation χ:
χ : Qi* → Qi0
That is, the deliverable for this phase is Core Qi.
Qi Compilation
Each individual compilation pass is a syntax transformation IR_i → IR_i+1 for some intermediate languages IR_i and IR_i+1. The initial input to this pipeline is Qi0 (i.e. IR_0 = Qi0), and the final output is Racket (i.e. IR_n = Racket), so that the compilation phase as a whole is a transformation ξ:
ξ : Qi0 → Racket
That is, the deliverable for this phase is Racket.
As long as the process starts with Qi0 and delivers Racket, there are no constraints on what the intermediate languages IR_i could be. For instance, they could all be Qi0 (except the last), so that the rules are mainly nonlocal optimizations mapping combinations of Qi expressions into optimized versions. Or only some of them could be Qi0 (e.g. the early stages, consisting of nonlocal optimizations), or it could even be that IR_1 ... IR_n-1 don't resemble either Qi or Racket at all -- the point being just that there are no constraints here.
Implementation
Inserting the Compiler into the Lifecycle
Michael Ballantyne @michaelballantyne writes:
"Here's an example of how the compiler in flow might be transformed into a compile-time function; I'll just show one case for all:
So all that's left is to write the macro expander expand-flow."
Facilitating the Macro Expansion Phase
Michael Ballantyne writes:
"The binding spec DSL [...] implements the macro expander part. If you wrote it by hand, it would look something like this, again just showing the case for all:
plus a macro expansion clause like the one you've added to flow right now.
The clauses for all the various Qi0 forms each follow the same pattern: match the syntax, recur on subexpressions, and reconstruct syntax. The declarative DSL implements this more concisely. (And when your language has binding forms, handles scope and binding too.)"
Deforestation (suggested by @soegaard in Discord, though Sorawee pointed out that it may not be sound for strict languages. A paper, also from @soegaard, that proposes an approach for call-by-value languages (like Qi): https://arxiv.org/abs/1005.5278)
Once a reliable baseline is available for Qi's performance, we will want to write a Qi-specific compiler that operates on the generated Qi code in the expansion step, to produce optimized Racket expansions.
Overview
Qi will soon have first class macro-extensibility, which means that Qi macros written by users would be indistinguishable from built-in Qi forms, allowing extension in arbitrary ways. At that stage, many of Qi's current "core" forms could themselves be implemented internally as such macros, which would expand into a small core language yet to be distilled.
In tandem with that effort, we also will want to write a compiler that will enter the lifecycle after the macro expansion phase and expand the resulting Qi forms into optimized Racket code for improved performance.
Details
Define
Qi*
to be the full Qi language including both the core forms as well as any macros, and letQi0
be a "core" Qi language, representing the end state of Qi macro expansion. Then, the full lifecycle of a Qi expression before it is evaluated is:... followed by the usual Racket lifecycle which it mirrors:
More: Fully Expanded Racket Racket bytecode
Qi Macro Expansion Phase
Each individual macro expansion rule is a syntax transformation
Qi* → Qi*
. The macro expansion phase as a whole (i.e. with repeated application of transformation rules) is a transformationχ
:That is, the deliverable for this phase is Core Qi.
Qi Compilation
Each individual compilation pass is a syntax transformation
IR_i → IR_i+1
for some intermediate languages IR_i and IR_i+1. The initial input to this pipeline isQi0
(i.e.IR_0 = Qi0
), and the final output is Racket (i.e.IR_n = Racket
), so that the compilation phase as a whole is a transformationξ
:That is, the deliverable for this phase is Racket.
As long as the process starts with Qi0 and delivers Racket, there are no constraints on what the intermediate languages IR_i could be. For instance, they could all be Qi0 (except the last), so that the rules are mainly nonlocal optimizations mapping combinations of Qi expressions into optimized versions. Or only some of them could be Qi0 (e.g. the early stages, consisting of nonlocal optimizations), or it could even be that IR_1 ... IR_n-1 don't resemble either Qi or Racket at all -- the point being just that there are no constraints here.
Implementation
Inserting the Compiler into the Lifecycle
Michael Ballantyne @michaelballantyne writes:
"Here's an example of how the compiler in
flow
might be transformed into a compile-time function; I'll just show one case forall
:The overall compiler would compose the code generator with other passes:
Finally, the new flow macro would compose a macro expander for Qi with the compiler:
So all that's left is to write the macro expander
expand-flow
."Facilitating the Macro Expansion Phase
Michael Ballantyne writes:
"The binding spec DSL [...] implements the macro expander part. If you wrote it by hand, it would look something like this, again just showing the case for
all
:plus a macro expansion clause like the one you've added to
flow
right now.The clauses for all the various Qi0 forms each follow the same pattern: match the syntax, recur on subexpressions, and reconstruct syntax. The declarative DSL implements this more concisely. (And when your language has binding forms, handles scope and binding too.)"
(For reference, the binding spec DSL.)
Strategies