In this section, we formally define each of the transformation steps informally described in Section 2. For each transformation function, we list only the most relevant cases; the remaining cases trivially recurse on the A-normal form (ANF) abstract syntax. We annotate functions with E, \( CE \), and \( AE \) to indicate the corresponding ANF syntactic classes. We omit annotations when a function only operates on a single syntactic class. For readability, we annotate meta-variables to hint at their intended use – \(\rho \) stands for read-only entities (such as environments), whereas \(\sigma \) stands for read-write or “state-like” entities of a configuration (e.g., stores or exception states). These can be mixed with our notation for syntactic lists, so, for example, \(\vec {x}^\sigma \) is a sequence of variables referring to state-like entities, while \(\vec {ae}^\rho \) is a sequence of a-expressions corresponding to read-only entities.
4.1 CPS Conversion
The first stage of the process is a partial CPS conversion [8, 25] to make control flow in the evaluator explicit. We limit this transformation to the main evaluator function, i.e., only the function
will take an additional continuation argument and will pass results to it. Because our input language is already in ANF, the conversion is relatively easy to express. In particular, applications of the evaluator are always
-bound to a variable (or appear in a tail position), which makes constructing the current continuation straightforward. Below are the relevant clauses of the conversion. For this transformation we assume the following easily checkable properties:
-
The evaluator name is globally unique.
-
The evaluator is never applied partially.
-
All bound variables are distinct.
The conversion is defined as three mutually recursive functions with the following signatures:
In the equations, \(\mathcal {K},\; \mathcal {I},\; \mathcal {A}_k : CExpr \rightarrow Expr \) are meta-continuations; \(\mathcal {I}\) injects a \( CExpr \) into \( Expr \).
where for any k, \(\mathcal {A}_k\) is defined as
and
In the above equations,
is a pseudo-construct used to make renormalization more readable. In essence, it is a non-ANF version of
where the bound expression is generalized to \( Expr \). Note that
only works correctly if
, which is implied by our assumption that all bound variables are distinct.
4.2 Generalization of Continuations
The continuations resulting from the above CPS conversion expect to be applied to value terms. The next step is to generalize (or “lift”) the continuations so that they recursively call the evaluator to evaluate non-value arguments. In other words, assuming the term type can be factored into values and computations \(V + C\), we convert each continuation k with the type
into a continuation
using the following schema:
The recursive clauses will correspond to congruence rules in the resulting small-step semantics.
The transformation works by finding the unique application site of the continuation and then inserting the corresponding call to
in the non-value case.
where
-
is the unique use site of the continuation k in expression e, that is, the \( CExpr \) where
is applied with k as its continuation; and
-
\(\hat{x}\) is a fresh variable associated with x – it stands for “a term corresponding to (the value) x”.
Following the CPS conversion, each named continuation is applied exactly once in e, so
is total and returns the continuation’s unique use site. Moreover, because the continuation was originally defined and let-bound at that use site, all free variables in
are also free in the definition of k.
When performing this generalization transformation, we also modify tail positions in
that return a value so that they wrap their result in the
constructor. That is, if the continuation parameter of
is k, then we rewrite all sites applying k to a configuration as follows:
4.3 Argument Lifting in Continuations
In the next phase, we partially lift free variables in continuations to make them explicit arguments. We perform a selective lifting in that we avoid lifting non-term arguments to the evaluation function. These arguments represent entities that parameterize the evaluation of a term. If an entity is modified during evaluation, the modified entity variable gets lifted. In the running example of Section 2, such a lifting occurred for \( kclo _1\).
Function
specifies the transformation at the continuation definition site:
where
-
\(\varXi ' = \varXi \cup \{ k \}\)
-
-
\(\varDelta ' = \varDelta [k \mapsto (x_1, \ldots , x_n)]\)
and at the continuation application site – recall that continuations are always applied fully, but at this point they are only applied to one argument:
if
and \(\varDelta (k) = (x_1, \ldots , x_n)\).
Our lifting function is a restricted version of a standard argument-lifting algorithm [19]. The first restriction is that we do not lift all free variables, since we do not aim to float and lift the continuations to the top-level of the program, only to the top-level of the evaluation function. The other difference is that we can use a simpler way to compute the set of lifted parameters due to the absence of mutual recursion between continuations. The correctness of this can be proved using the approach of Fischbach [16].
4.4 Continuations Switch Control Directly
At this point, continuations handle the full evaluation of a term themselves. Instead of calling
with the continuation as an argument, we can call the continuation directly to switch control between evaluation stages of a term. We will replace original
call sites with direct applications of the corresponding continuations. The recursive call to
in congruence cases of continuations will be left untouched, as this is where the continuation’s argument will be evaluated to a value. Following from the continuation generalization transformation, this call to
is with the same arguments as in the original site (which we are now replacing). In particular, the
is invoked with the same \(\vec {ae}^\rho \) arguments in the continuation body as in the original call site.
4.5 Defunctionalization
Now we can move towards a first-order representation of continuations which can be further converted into term constructions. We defunctionalize continuations by first collecting all continuations in
, then introducing corresponding constructors (the syntax), and finally generating an
function (the semantics). The collection function accumulates continuation names and their definitions. At the same time it removes the definitions.
We reuse continuation names for constructors. The
function is generated by simply generating a case analysis on the constructors and reusing the argument names from the continuation function arguments. In addition to the defunctionalized continuations, the generated
function will take the same arguments as
. Because of the absence of mutual recursion in our meta-language,
takes
as an argument.
Now we need a way to replace calls to continuations with corresponding calls to
. For \(\vec {ae}^\rho \) and \(k_{ top }\) we use the arguments passed to
or
(depending on where we are replacing).
Finally, the complete defunctionalization is defined in terms of the above three functions.
4.6 Remove Self-recursive Tail-Calls
This is the transformation which converts a recursive evaluator into a stepping function. The transformation itself is very simple: we simply replace the self-recursive calls to
in congruence cases.
Note, that we still leave those invocations of
that serve to switch control through the stages of evaluation. Unless a continuation constructor will become a part of the output language, its application will be inlined in the final phase of our transformation.
4.7 Convert Continuations to Terms
After defunctionalization, we effectively have two sorts of terms: those constructed using the original constructors and those constructed using continuation constructors. Terms in these two sorts are given their semantics by the
and
functions, respectively. To get only one evaluator function at the end of our transformation process, we will join these two sorts, adding extra continuation constructors as new term constructors. We could simply merge
to
, however, this would give us many overlapping constructors. For example, in Section 2, we established that
and
. The inference of equivalent term constructors is guided by the following simple principle. For each continuation term \(c^\textsc {k}(ae_1, \ldots , ae_n)\) we are looking for a term \(c'(ae'_1, \ldots , ae'_m)\), such that, for all \(\vec {ae}^\sigma \), \(\vec {ae}^\rho \) and \(ae_k\)
In our current implementation, we use a conservative approach where, starting from the cases in
, we search for continuations reachable along a control flow path. Variables appearing in the original term are instantiated along the way. Moreover, we collect variables dependent on configuration entities (state). If control flow is split based on information derived from the state, we automatically include any continuation constructors reachable from that point as new constructors in the resulting language and interpreter. This, together with how information flows from the top-level term to subterms in congruence cases, preserves the coupling between state and corresponding subterms between steps.
If, starting from an input term \(c(\vec {x})\), an invocation of
on a continuation term \(c^\textsc {k}(\vec {ae}_k)\) is reached, and if, after instantiating the variables in the input term \(c(\vec {ae})\), the sets of their free variables are equal, then we can introduce a translation from \(c^\textsc {k}(\vec {ae}_k)\) into \(c(\vec {ae})\). If such a direct path is not found, the \(c^\textsc {k}\) will become a new term constructor in the language and a case in
is introduced such that the above equation is satisfied.
4.8 Inlining, Simplification and Conversion to Direct Style
To finalize the generation of a small-step interpreter, we inline all invocations of
and simplify the final program. After this, the interpreter will consists of only the
function, still in continuation-passing style. To convert the interpreter to direct style, we simply substitute
’s continuation variable for (
) and reduce the new redexes. Then we remove the continuation argument performing rewrites following the scheme:
Finally, we remove the reflexive case on values (i.e.,
. At this point we have a small-step interpreter in direct form.
4.9 Removing Vacuous Continuations
After performing the above transformation steps, we may end up with some redundant term constructors, which we call “empty” or vacuous. These are constructors which only have one argument and their semantics is equivalent to the argument itself, save for an extra step which returns the computed value. In other words, they are unary constructs which only have two rules in the resulting small-step semantics matching the following pattern.
Such a construct will result from a continuation, which, even after generalization and argument lifting, merely evaluates its sole argument and returns the corresponding value:
These continuations can be easily identified and removed once argument lifting is performed, or at any point in the transformation pipeline, up until
is absorbed into
.
4.10 Detour: Generating Pretty-Big-Step Semantics
It is interesting to see what kind of semantics we get by rearranging or removing some steps of the above process. If, after CPS conversion, we do not generalize the continuations, but instead just lift their arguments and defunctionalize them,Footnote 1 we obtain a pretty-big-step [6] interpreter. The distinguishing feature of pretty-big-step semantics is that constructs which would normally have rules with multiple premises are factorized into intermediate constructs. As observed by Charguéraud, each intermediate construct corresponds to an intermediate state of the interpreter, which is why, in turn, they naturally correspond to continuations. Here are the pretty-big-step rules generated from the big-step semantics in Fig. 2 (Section 2).
As we can see, the evaluation of
now proceeds through two intermediate constructs,
, which correspond to continuations introduced in the CPS conversion. The evaluation of
starts by evaluating \(e_1\) to \(v_1\). Then
is responsible for evaluating \(e_2\) to \(v_2\). Finally,
evaluates the closure body just as the third premise of the original rule for
. Save for different order of arguments, the resulting intermediate constructs and their rules are identical to Charguéraud’s examples.
4.11 Pretty-Printing
For the purpose of presenting and studying the original and transformed semantics, we add a final pretty-printing phase. This amounts to generating inference rules corresponding to the control flow in the interpreter. This pretty-printing stage can be applied to both the big-step and small-step interpreters and was used to generate many of the rules in this paper, as well as for generating the appendix of the full version of this paper [1].
4.12 Correctness
A correctness proof for the full pipeline is not part of our current work. However, several of these steps (partial CPS conversion, partial argument lifting, defunctionalization, conversion to direct style) are instances of well-established techniques. In other cases, such as generalization of continuations (Section 4.2) and removal of self-recursive tail-calls (Section 4.6), we have informal proofs using equational reasoning [1]. The proof for tail-call removal is currently restricted to compositional interpreters.