The CEK Machine
This page presents the operational semantics of the Plutus language using an abstract machine - a CEK machine. Although more complex than traditional reduction semantics, the CEK machine offers an efficient evaluation strategy that enables precise modeling of resource usage and cost. The production Haskell evaluator is based on the CEK machine, and it also provides a practical foundation for alternative implementations.
The following listing defines some key concepts of the CEK machine.
Ξ£ β State ::= π ; π β³ π Computing M under environment π with stack π
| π β² π Returning a value π to stack π
| β¬₯ Throwing an error
| β»π Final state with result π
π β Stack ::= π* // A stack has zero or more stack frames
π β CEK value ::= γcon T πγ A constant π with type T
| γdelay π πγ A delayed computation, with an
associated environment
| γlam π₯ π πγ A lambda abstraction, with an
associated environment
| γconstr π π*γ A constructor application, where
all arguments are values
| γbuiltin π π* πγ A builtin application with all supplied
arguments as values, and expecting
at least one more argument
π β Environment ::= [] An empty environment
| π[π₯ β¦ π] Associate π₯ with π in the environment
π β Expected builtin arguments ::= [π] // One argument
| πβ
π // Two or more arguments
π β Frame ::= (force _) Awaiting a delayed computation to be forced
| [_ (π, π)] An application awaiting the function, where the
argument is a term associated with an environment
| [_ π] An application awaiting the function, where the
argument is a value
| [π _] An application awaiting the argument, where the
function is a value
| (constr π π* _ (π*, π)) A constructor application awaiting
an argument. The arguments before the hole
are values, and the arguments after
are terms to be evaluated.
| (case _ (π*, π)) A case expression awaiting the scrutinee
The CEK machine has two main kinds of states:
π ; π β³ π
denotes evaluating termπ
with environmentπ
and stackπ
.π β² π
denotes returning a valueπ
to stackπ
.
A value is a fully evaluated term, plus environments necessary for further computation.
An environment is a map binding variables to values.
A stack frame contains a hole to represent a pending value, and the context needed to continue evaluation once the value is received.
A builtin argument π
is either a term or a type argument.
To evaluate a Plutus program containing a term π
, the machine starts from state []; [] β³ π
, and based on the following transition table, proceeds as follows:
- If the current CEK machine state matches the From State, and the associated condition (if exists) is met, then the CEK machine transitions into the To State.
- If the machine arrives at state
β»π
, the machine terminates with success, yielding the Plutus term corresponding toπ
(which is essentiallyπ
but with the environments removed) as final result. - If the machine gets stuck (i.e., no rule applies) or arrives at state
β¬₯
, the evaluation terminates with a failure.
Rule | From State | To State | Condition |
---|---|---|---|
1 | π ; π β³ π₯ | π β² π[π₯] | π₯ is bound in π |
2 | π ; π β³ (con T π) | π β² γcon T πγ | |
3 | π ; π β³ (lam π₯ π) | π β² γlam π₯ π πγ | |
4 | π ; π β³ (delay π) | π β² γdelay π πγ | |
5 | π ; π β³ (force π) | (force _)β
π ; π β³ π | |
6 | π ; π β³ [π π] | [_ (π, π)]β
π ; π β³ π | |
7 | π ; π β³ (constr π πβ
π*) | (constr π _ (π*, π))β
π ; π β³ π | |
8 | π ; π β³ (constr π []) | π β² γconstr π []γ | |
9 | π ; π β³ (case π π*) | (case _ (π*, π))β
π ; π β³ π | |
10 | π ; π β³ (builtin π) | π β² γbuiltin π [] πΌ(π)γ | |
11 | π ; π β³ (error) | β¬₯ | |
12 | [] β² π | β»π | |
13 | [_ (π, π)]β
π β² π | [π _]β
π ; π β³ π | |
14 | [γlam π₯ π πγ _]β
π β² π | π ; π[π₯ β¦ π] β³ π | |
15 | [_ π]β
π β² γlam π₯ π πγ | π ; π[π₯ β¦ π] β³ π | |
16 | [γbuiltin π π* (πβ
π)γ _]β
π β² π | π β² γbuiltin π (πβ
π*) πγ | π is a term argument |
17 | [_ π]β
π β² γbuiltin π π* (πβ
π)γ | π β² γbuiltin π (πβ
π*) πγ | π is a term argument |
18 | [γbuiltin π π* [π]γ _]β
π β² π | π€ππΊπ
π’π€πͺ (π , π, π*β
π) | π is a term argument |
19 | [_ π]β
π β² γbuiltin π π* [π]γ | π€ππΊπ
π’π€πͺ (π , π, π*β
π) | π is a term argument |
20 | (force _)β
π β² γdelay π πγ | π ; π β³ π | |
21 | (force _)β
π β² γbuiltin π π* (πβ
π)γ | π β² γbuiltin π π* πγ | π is a type argument |
22 | (force _)β
π β² γbuiltin π π* [π]γ | π€ππΊπ
π’π€πͺ (π , π, π*) | π is a type argument |
23 | (constr π π* _ (πβ
π*, π))β
π β² π | (constr π π*β
π _ (π*, π))β
π ; π β³ π | |
24 | (constr π π _ ([], π))β
π β² π | π β² γconstr π π*β
π γ | |
25 | (case _ (πβ β¦ πβ , π))β
π β² γconstr π πβ β¦ πβγ | [_ πβ]β
β―β
[_ πβ]β
π ; π β³ ππ | 0 β€ π β€ π |
In this table, X*
denotes a list of X
s.
The symbol β
denotes either the cons or snoc operator on lists.
Explanation of the transition rules:
- To evaluate a single variable
π₯
, it looks up its value in the environmentπ
, and returns the value if exists. - A constant evaluates to itself, as it is already a value.
- A lambda abstraction evaluates to itself, as it is already a value.
The environment is captured in the returned value, for later use in computing
π
. - A delayed computation evaluates to itself, as it is already a value.
The environment is captured in the returned value, for later use in computing
π
. - To evaluate
(force π)
, it pushes a frame(force _)
onto the stack, and evaluatesπ
. Afterπ
is evaluated to a value, the forcing will continue (rules 20, 21 and 22) depending on what the value is. - To evaluate an application
[π π]
, the machine first evaluates the functionπ
, after pushing frame[_ (π, π)]
onto the stack. This ensures that onceπ
is evaluated, it will proceed to evaluate the argumentπ
using the same environment. - To evaluate a constructor application, the machine pushes a frame onto the stack with a hole in place of the first argument.
The frame also stores the remaining arguments along with the current environment.
It then proceeds to evaluate the first argument
π
. - A nullary constructor evaluates to itself, as it is already a value.
- To evaluate a
case
expression, the machine pushes a frame onto the stack with a hole in place of the scrutinee. The frame also stores the branches,π*
, along with the current environment. It then proceeds to evaluate the scrutineeπ
. - A builtin function evaluates to itself as it is already a value.
- Evaluating
(error)
results in the machine terminating with a failure. - When a value
π
is returned to an empty stack, the machine terminates with success, yieldingπ
as final result. - When a value
π
is returned to a stack whose top frame represents an application with the hole in the function position, the frame is replaced with one where the function isπ
and the hole is in the argument position. The machine then continues by evaluating the argumentπ
in the captured environment. - When a value
π
is returned to a stack whose top frame represents an application, where the hole is in the argument position and the function is a lambda abstraction, the machine pops the frame, extends the environment to bindπ₯
toπ
, and continues by evaluatingπ
. This corresponds to beta reduction. - If the returned value is a lambda abstraction, and the top stack frame represents an application, where the hole is in the function position and the argument is a value, the machine proceeds in the same way as the previous rule.
- When a value
π
is returned to a stack whose top frame represents an application where the hole is in the argument position, and the function is a builtin expecting at least two more arguments (sinceπ
is a non-empty list,πβ π
contains at least two elements) and the first is a term argument, the machine pops the frame, and returns an updatedbuiltin
value. Because the builtin still requires at least one more argument, the builtin application cannot yet be evaluated and is therefore considered a value. - If the returned value is a builtin application expecting at least two arguments, where the first is a term argument, and the top stack frame represents an application, where the hole is in the function position and the argument is a value, the machine proceeds in the same way as the previous rule.
- Like Rule 16, except that the builtin expects exactly one more argument, and it is term argument.
In this case the builtin application is now saturated, so it is evaluated via
π€ππΊπ π’π€πͺ
. The mechanism ofπ€ππΊπ π’π€πͺ
is described later. - Like Rule 17, except that the builtin expects exactly one more argument, and it is term argument. The machine proceeds in the same way as the previous rule.
- If the returned value is a delayed computation, and the top stack frame is a
force
frame, then theforce
anddelay
cancel each other, and the machine continues by evaluating theπ
in the captured environment. - If the returned value is a builtin application expecting at least two arguments, where the first is a type argument, and the top stack frame is a
force
frame, the machine pops the frame, and returns an updated builtin application value, with the first argument removed. In Plutus, forcing corresponds to applying a type argument. A builtin application expecting a type argument must be forced before evaluation can continue. - Like Rule 21, except that the builtin excepts exactly one more argument, and it is type argument.
In this case the
force
makes the builtin application saturated, so it is evaluated viaπ€ππΊπ π’π€πͺ
. - When a value
π
is returned to a stack whose top frame is a constructor application, with the hole in any argument position except the last (in other words, there is at least one more argument to be evaluated), the machine replaces the frame with one where the hole is moved to the next argument, and proceeds to evaluate the next argumentπ
in the captured environment. - Like Rule 23, except that the hole is in the position of the last argument.
In this case, all arguments have been evaluated, so the machine pops the frame and returns a
constr
value. - If the returned value is a constructor application with index
π
, and the top stack frame is acase
frame, the machine will evaluate theπ
th branch -ππ
- applied to argumentsπβ β¦ πβ
(it is important to note that arguments underconstr
are reversed when passing to acase
branch - this is done for performance reasons). To do so, it pops the frame, and pushesm
frames, each representing an application, with the top frame corresponding toπβ
(the first argument).