2.2 The Language
Having seen how Sequent Core is a language resembling an abstract
machine, let’s look more closely at the new linguistic concepts that
it introduces and how Sequent Core compares to Core. On closer
inspection, Sequent Core can be seen as a nuanced variation on
Core, separating the roles of distinct concepts of Core syntactically
as part of the effort to split calculations across the two sides of a cut
pair. More specifically, each construct of Core has an exact analogue
in Sequent Core, but the single grammar of Core expressions
e
is
divided among terms
v
, continuations
k
, and commands
c
in Sequent
Core. Additionally, Sequent Core has special support for labels and
direct jumps, which are not found in Core.
2.2.1 Terms and Continuations
Core expressions
e
, as shown in Figure 1, include a variety of
values (more specifically weak-head normal forms) which require
no further evaluation: lambdas (both small
λ
and big
Λ
) and
applied constructors. Along with variables, these are all terms in
Sequent Core, as they do not involve any work to be done and they
immediately produce themselves as their result.
On the other hand, Core also includes expressions which do
require evaluation: function applications
e e
0
, polymorphic instan-
tiations
e τ
, and
case
expressions. Each of these expressions uses
something to create the next result, and thus these are reflected as
continuations
k
in Sequent Core. As usual, Sequent Core contin-
uations represent evaluation contexts that receive an input which
will be used immediately. For example, the application context
1
,
where “
” is the hole where the input is placed, corresponds to the
call stack
1 · ret
. Furthermore, we can apply the curried function
λx.λy.x
to the arguments
1
and
2
by running it in concert with the
stack 1 · 2 · ret, as in:
hλx.λy.x || 1 · 2 · reti = hλy.1 || 2 · reti = h1 || reti
where ret signals a stop, so that the result 1 can be returned.
Since we are interested in modeling lazy functional languages,
we also need to include the results of arbitrary deferred computations
as terms in themselves. For example, when we perform the lazy
function composition
f (g x)
in Core,
g x
is only computed when
f
demands it. This means we need the ability to inject computations
into terms, which we achieve with
µ
-abstractions. A
µ
-abstraction
extracts a result from a command by binding the occurrences of
ret
in that command, so that anything passed to
ret
is returned from
the
µ
-abstraction. However, because we are only modeling purely
functional programs, there is only ever one
ret
available at a time,
making it a rather limited namespace. Thus,
µret. hg || x · reti
runs
the underlying command, calling the function
g
with the argument
x
, so that whatever is returned by
g
pops out as the result of the
term. So lazy function composition can be written in Sequent Core
as hf || (µret. hg || x · reti) · reti.
Notice that every closed command must give a result to
ret
if it
ever stops at all. Another way of looking at this fact is that every
(finite) continuation has
ret
“in the tail”; it plays the role of “nil”
in a linked list. However, the return structure of continuations is
more complex than a plain linked list, since the terminating
ret
of a continuation may occur in several places. By inspection, a
continuation is a sequence of zero or more type or term applications,
followed by either
ret
itself or by a
case
continuation. But in the
latter case, each alternative has a command whose continuation must
in turn have
ret
in the tail. Unfortunately, this analogy breaks down
in the presence of local bindings, as we will see. Luckily, however,
viewing
ret
as a static variable bound by
µ
-abstractions tells us
exactly how to “chase the tail” of a continuation by following the
normal rules of static scope. So we may still say that every closed
computation hv || ki eventually returns if it does not diverge.
2.2.2 Bindings and Jumps
There is one remaining Core expression to be sorted into the Sequent
Core grammar:
let
bindings. In Sequent Core,
let
bindings are
commands, as they set up an enclosing environment for another
command to run in, forming an executable code block. In both
representations,
let
bindings serve two purposes: to give a shared
name to the result of some computation, and to express (mutual)
recursion. Thus in the Sequent Core command
c
, we can share
the results of terms through
let x = v in c
and we can share a
continuation through
hµret.c || ki
. But something is missing. How
can we give a shared label to a command (i.e., to a block of code)
that we can go to during the execution of another command? This
facility is critical for maintaining small code size, so that we are not
forced to repeat the same command verbatim in a program.
For example, suppose we have the command
hz || case of Left(x) → c, Right(x) → ci
wherein the same
c
is repeated twice due to the
case
continuation.
Now, how do we lift out and give a name to
c
, given that it contains
the free variable
x
? We would rather not use a lambda, as in
λx.µret.c
, since that introduces additional overhead compared to
the original command. Instead, we would rather think of
c
as a sort
of continuation whose input is named
x
during execution of
c
. In
the syntax of
λµ˜µ
[
9
], this would be written as
˜µx.c
, the dual of
µ
-abstractions. However, this is not like the other continuations we
have seen so far! There is no guarantee that
˜µx.c
uses its input
immediately, or even at all. Thus, we are not dealing with an
evaluation context, but rather an arbitrary context. Furthermore,
we might (reasonably) want to name commands with multiple
free variables, or even free type variables. So in actuality, we are
looking for a representation of continuations taking multiple values
as inputs of polymorphic types, corresponding to general contexts
with multiple holes.
This need leads us to multiple-input continuations, which we
write as
˜µ[a
1
, . . . , a
n
, x
1
, . . . , x
m
].c
in the style of
λµ˜µ
. These con-
tinuations accept several inputs (named
x
1
. . . x
m
), whose types are
polymorphic over the choice of types for
a
1
. . . a
n
, in order to run a
command
c
. Intuitively, we may also think of these multiple-input
continuations as a sequence of lambdas
Λa
1
. . . a
n
.λx
1
. . . x
m
.c
,
except that the body is a command because it does not return.
The purpose of introducing multiple-input continuations was to
lift out and name arbitrary commands, and so they appear as a Se-
quent Core binding. Specifically, all multiple-input continuations
in Sequent Core are given a label
j
, as in the continuation binding
j = ˜µ[x, y]. h(+) || x · y · reti
. These labeled continuations serve
as join points: places where the control flow of several diverging
branches of a program joins back up again.
In order to invoke a bound continuation, we can jump to it by
providing the correct number of terms for the inputs, as well as
explicitly specifying the instantiation of any polymorphic type in
System Fω style. For example, the command
let j = ˜µ[a:?, x:a, f:a → Bool ]. hf || x · reti
in jump j Bool True not
will jump to the label
j
with the inputs
Bool
,
True
, and
not
,
which results in
hnot || True · reti
. So when viewing Sequent Core
from the perspective of an abstract machine, its command language
provides three instructions: (1) set a binding with
let
, (2) evaluate
an expression with a cut pair, or (3) perform a direct jump.
Take note that a labeled continuation does not introduce a
µ
-
binder. As a consequence, the occurrence of
ret
found in
j =
˜µ[x, y]. h(+) || x · y · reti
refers to the nearest surrounding
µ
, unlike
the
ret
found in
f = λx.λy.µret. h(+) || x · y · reti
. Viewing
ret
as a statically bound variable means that labeled continuations
participate in the “tail chasing” discussed previously in Section 2.2.1.
76