Compilers that target hosts with a hard statement/expression split (JavaScript, C, and similar "bracket" languages) have to respect where values are allowed and where only effects make sense. ClojureScript's emitter handled this with context-sensitive emission: every node got an expression-or-statement flag and picked a matching output form. I patterned JACL after that mode because it kept the emitter easy to read while the backend was taking shape. (See the ClojureScript emitter :context handling.)
Context-sensitive emission (expression/statement mode)
Destination-driven compilation (value/effect/tail)
returns.return.Example: compiling a value-binding if in statement position
The IR sketches use tagged s-expressions; ctx shows the expression/statement flag carried on each node, and dest marks the value/effect/tail destination threaded through the tree.
Source (Lisp-ish):
(let [x (if test (f) (g))]
(h x))
Context-sensitive IR (expression mode needing a value):
(ctx :stmt
(let-binding x
(ctx :expr
(if-expr (ctx :expr (var test))
(ctx :expr (call f))
(ctx :expr (call g))))
(ctx :stmt (call h (ctx :expr (var x))))))
Context-sensitive emission (expression mode needing a value):
var x = (function(){
if (test) { return f(); }
else { return g(); }
})();
h(x);
Destination-driven IR (value destination):
(dest :effect
(let-binding x
(dest :value
(if-node (var test)
(dest :value (call f))
(dest :value (call g))))
(dest :tail (call h (var x)))))
Destination-driven emission (value destination):
var x;
if (test) { x = f(); }
else { x = g(); }
h(x);
Destination-driven output already looks like the optimized version. If I rewrote JACL to target destinations instead of expression-versus-statement mode, I expect fewer IIFEs, fewer temps, and fewer cleanup passes to bridge the abstraction mismatch with bracketed hosts.
Context-sensitive emission is also the easier compiler to write: it mirrors the host surface syntax and is a more intuitive first refactor when you just need the emitter to land. The tradeoff is runtime baggage from scaffolding and wrappers. On a JIT'd platform (JVM, JavaScript) the baggage tends to get compiled away, so the simpler emitter can be a deliberate choice rather than a naive one. If you're targeting C or another ahead-of-time host, the overhead sticks around and the destination-driven style starts to pay for itself.
Background reading
- Destination-driven code generation was popularized by Kent Dybvig (see "Destination-Driven Code Generation").
- Context-sensitive emission in ClojureScript is visible in its compiler emitter path that threads a :context flag per node.