Middle-end

Lowering Language Features

Closures, coroutines, async, exceptions and match expressions are all ordinary control flow after the compiler is done with them. This module does the transformation.

Syntactic Sugar
▶ lab

Sugar changes how a program is written without changing what the language can express. That makes it cheap to add and easy to underrate — the cost is not in the semantics, it is in the grammar, the diagnostics and the number of ways to say the same thing.

Q · What actually makes a feature "just syntactic sugar", and does calling it that mean it does not matter?
Desugaring
▶ lab

The pass that rewrites high-level syntax into a core language every later phase can assume. Doing it early makes every later phase simpler; doing it early is also how compilers end up reporting errors about code nobody wrote.

Q · When in the pipeline should high-level syntax be rewritten into the core language, and what does the choice cost?
Closures

A function value that refers to a variable from an enclosing scope keeps that variable alive after the enclosing frame is gone. The language question is what the closure captures; the compiler question is where the captured variable now lives.

Q · If a function returns a lambda that uses a local variable, where does that variable live after the function returns?
Closure Conversion
▶ lab

The transformation that turns a closure into an explicit pair of code and environment record. The whole design rests on one question — does the environment hold the values or the bindings — and the classic JavaScript loop bug is what that question looks like when you get it wrong.

Q · What does the compiler actually build when it sees a lambda that captures a variable?
Lambda Lifting

The other way to remove a nested function: turn its free variables into extra parameters and lift it to the top level. No environment, no allocation — and it only works when the function does not escape.

Q · If I can pass captured variables as extra arguments, why does anyone build an environment record?
Lowering Coroutines
▶ lab

A function that can pause and resume cannot keep its locals in a stack frame, because the frame does not survive the pause. The compiler splits the function at every suspension point and moves the surviving locals into a heap object, turning the body into a resumable state machine.

Q · What does the compiler do to a function containing `yield` so that it can stop in the middle and continue later?
Lowering Async and Await
▶ lab

An async function is a coroutine whose resumptions are driven by completing operations rather than by a consumer asking for the next value. The transformation is the same state machine, plus a continuation: something has to know what to call when the awaited thing finishes.

Q · What does `async` actually do to a function, and why can I only `await` inside one?
Exception Handling

At the source level, a non-local jump out of an arbitrary depth of calls. At the implementation level, a choice between paying nothing until a throw and looking the answer up in a table, or paying a little on every entry and jumping straight there.

Q · What does `try`/`catch` compile to, and is it true that exceptions cost nothing until one is thrown?
Stack Unwinding

The mechanism underneath exceptions: walk the physical stack, and for each frame use compiler-emitted tables to restore the caller's registers, run that frame's cleanups, and decide whether it handles the exception. This is where "zero-cost" is paid for.

Q · How does the runtime know what to destroy and where to jump when an exception propagates through a frame that never mentions exceptions?
Compiling Pattern Matching
▶ lab

An ordered list of match arms is semantics, not implementation. The compiler turns it into a decision tree that tests each discriminant once — which is why a match is not a chain of comparisons, and why the naive reading of it is quadratic in the wrong place.

Q · Does a `match` really test every arm in order until one fits, and if not, what does it actually generate?