The UPLC language: terms, builtins, and versions

How smart contracts run explained that the ledger executes Untyped Plutus Core (UPLC) and how the CEK machine evaluates it. This page looks at the language itself: what a UPLC program looks like, the built-in functions (“primops”) it provides, and how the language has grown across Cardano’s hard forks.

You do not write UPLC by hand — it is a compilation target, the assembly of Cardano. But knowing its shape makes the rest of the system legible: it is a very small language, and almost all of its real capability lives in its builtins.

What a program looks like

A UPLC program is a version number wrapped around a single term:

(program 1.1.0 <term>)

The term language is tiny. It is the untyped lambda calculus plus a handful of extras:

  • variables — x
  • lambda abstraction — (lam x <body>)
  • application — [ <fun> <arg> ] (note the square brackets; it is curried, so [ [ f x ] y ] applies f to two arguments)
  • constants — (con <type> <value>), e.g. (con integer 42)
  • builtins — (builtin addInteger)
  • delay / force — (delay <term>) suspends evaluation; (force <term>) resumes it
  • error — (error), which aborts evaluation and fails the script

A small complete program that adds two integers:

(program 1.1.0
  [ [ (builtin addInteger) (con integer 2) ] (con integer 3) ]
)

Because the language is untyped, nothing stops a nonsensical term such as applying an integer as if it were a function; the machine simply evaluates such a term to (error) when it reaches it. The delay and force forms above look odd in isolation; they are tied to how UPLC evaluates, which the next section explains.

Evaluation order: strictness, delay, and force

UPLC is strict — call-by-value. When the CEK machine evaluates an application [ f x ], it reduces f to a function and then reduces x to a value before performing the call; the argument is always evaluated first. The same holds for the fields of a constr and the operands of a builtin: a subterm is evaluated as soon as it is reached. Only a few forms are values in their own right and are not evaluated until used — a (lam ...), a (con ...), a partially-applied (builtin ...), and, crucially, a (delay ...).

Strictness has a sharp consequence: there is no built-in laziness. If you express a conditional by passing both outcomes as ordinary arguments, both are evaluated before the conditional can choose — wasted work at best, and an outright failure if the branch not taken contains an (error). UPLC’s answer is to make suspension explicit:

  • (delay M) packages M as a value without evaluating it — a thunk.
  • (force t) runs a thunk: it resumes evaluation of the M inside a delay.

So force (delay M) evaluates M, and that round trip is the only way to defer a computation and later resume it.

Two jobs, one pair of operators

delay and force end up doing two distinct things, which is why compiled UPLC is so full of them:

  1. Laziness. Wrap a term in delay to stop strict evaluation from running it too early, and force it at the point you actually want it.
  2. Type instantiation. The typed languages that compile to UPLC are polymorphic: a function like ifThenElse has type forall a. bool -> a -> a -> a, and before use you must pick the concrete type a — in a typed language, by a type application. UPLC has no types, so there is nothing to pass; but the type applications cannot simply be deleted, because that would change when subterms evaluate. So type erasure rewrites every type abstraction to a (delay ...) and every type application to a (force ...). A polymorphic builtin is therefore instantiated by forcing it.

The standard conditional shows both jobs at once. ifThenElse is polymorphic, so it is forced once to instantiate it; and because evaluation is strict, the two branches are wrapped in delay so that only the chosen one runs, after which the result is forced:

(force
  [ [ [ (force (builtin ifThenElse)) <cond> ]
      (delay <then>) ]
    (delay <else>) ])

The inner (force (builtin ifThenElse)) instantiates the builtin (job 2); the two (delay ...) keep both branches unevaluated (job 1); ifThenElse returns whichever thunk matches <cond>; and the outer (force ...) runs just that one.

The builtin checklist

Because forces are real evaluation steps, the machine is exact about how many it expects. Every builtin carries a fixed sequence of steps, read off its type: each -> is a “supply an argument” step, each forall is a “force me” step.

addInteger : integer -> integer -> integer    steps: arg, arg
ifThenElse : forall a. bool -> a -> a -> a     steps: force, arg, arg, arg

The machine walks this sequence and expects exactly the next item. Supply an argument where a force is due (or force where an argument is due) and the builtin application is malformed: evaluation raises an error and the script fails. Stop early — never completing the sequence — and you are left with an unsaturated builtin value that simply never computes. Either way, a missing or stray force is never silently ignored: it is the erased type instantiation, and the machine counts every one.

Built-in types

Constants are drawn from a fixed set of built-in types:

  • integer (arbitrary precision), bytestring, string, unit, bool
  • (list T) and (pair T T) of other built-in types
  • data — the universal tree used for script arguments (see below)
  • the BLS12-381 elements bls12_381_G1_element, bls12_381_G2_element, and bls12_381_mlresult

The data type is the most important in practice. It is the encoding every datum and redeemer uses on the wire: a tree of tagged constructors, maps, lists, integers (I), and byte strings (B). The blueprint schema is precisely a description of which data shape a given script expects.

Built-in functions (primops)

The bulk of UPLC’s power is its default builtins. The original set, present since the first version, groups into:

  • Integers — addInteger, subtractInteger, multiplyInteger, divideInteger, quotientInteger, remainderInteger, modInteger, equalsInteger, lessThanInteger, lessThanEqualsInteger
  • Byte strings — appendByteString, consByteString, sliceByteString, lengthOfByteString, indexByteString, equalsByteString, lessThanByteString, lessThanEqualsByteString
  • Hashing and signatures — sha2_256, sha3_256, blake2b_256, verifyEd25519Signature
  • Strings — appendString, equalsString, encodeUtf8, decodeUtf8
  • Control — ifThenElse, chooseUnit, trace
  • Pairs — fstPair, sndPair
  • Lists — chooseList, mkCons, headList, tailList, nullList
  • Data — chooseData, constrData, mapData, listData, iData, bData, unConstrData, unMapData, unListData, unIData, unBData, equalsData, mkPairData, mkNilData, mkNilPairData

Each builtin carries a fixed cost in CPU and memory units, which is what makes metered execution possible. Later versions add more builtins, covered next.

Sum-of-products terms

UPLC language version 1.1.0 adds two term forms for sums of products, letting datatypes be encoded directly rather than via Scott encoding:

  • (constr i <term> ...) — build the i-th variant with the given fields
  • (case <term> <branch> ...) — branch on a constr value

These usually produce smaller, cheaper code than the lambda encodings they replace. Whether one of your datatypes is represented this way or directly as data is a choice — see Data representation.

How the language has grown

There are two version numbers in play, and they are easy to conflate:

  • The UPLC language version — the number in (program X.Y.Z ...). 1.0.0 is the original language; 1.1.0 adds the constr/case terms above.
  • The Plutus ledger language version — PlutusV1, PlutusV2, PlutusV3. This is what a script tags itself as on-chain, and it governs which builtins are allowed and what the script context looks like.

On top of that, individual builtins are switched on at specific Cardano hard forks (protocol versions), and the same builtin can become available to different ledger languages at different times. For the current PlutusV3 language the introductions run:

Hard fork (protocol version) Introduced for PlutusV3
Chang — 2024 (PV 9) UPLC 1.1.0 (constr/case); BLS12-381 G1/G2 and pairing ops, keccak_256, blake2b_224; integerToByteString, byteStringToInteger; plus the original set and serialiseData, verifyEcdsaSecp256k1Signature, verifySchnorrSecp256k1Signature
Plomin — 2025 (PV 10) logical/bitwise byte-string ops (andByteString, orByteString, xorByteString, complementByteString, shiftByteString, rotateByteString, readBit, writeBits, replicateByte, countSetBits, findFirstSetBit); ripemd_160
van Rossem — upcoming (PV 11) (planned) expModInteger, dropList, array ops (lengthOfArray, listToArray, indexArray), BLS multi-scalar multiplication, and Value operations

The older ledger languages started smaller:

  • PlutusV1 (Alonzo, 2021) launched with the original builtin set and UPLC 1.0.0 only.
  • PlutusV2 (Vasil, 2022) added serialiseData, and at the Valentine hard fork (2023) the two Secp256k1 signature builtins.

The van Rossem hard fork (protocol version 11, targeted for 2026 and not yet live at the time of writing) is set to unify the builtins: it makes the full set available across PlutusV1, PlutusV2, and PlutusV3 and brings UPLC 1.1.0 to all three. After it, the languages will differ chiefly in their script context rather than in which builtins they offer.

A few of these have specific motivations worth noting: keccak_256 and ripemd_160 provide Ethereum- and Bitcoin-compatible hashing; the BLS12-381 operations enable pairing-based cryptography (threshold signatures, zero-knowledge proofs); and integerToByteString/byteStringToInteger plus the bitwise ops let contracts implement cryptographic primitives efficiently in-script.

Ledger language versions and the script context

What now most distinguishes PlutusV1, PlutusV2, and PlutusV3 is the shape of the script context the validator receives:

  • PlutusV1 — the original transaction view: inputs, outputs, fee, mint, certificates, withdrawals, validity interval, signatories, datums, and the transaction id.
  • PlutusV2 — extends it with the features the Vasil ledger introduced: reference inputs, inline datums, reference scripts, and the full map of redeemers.
  • PlutusV3 — reworks the context for governance: it adds votes, proposal procedures, and treasury fields, and generalises the notion of what a script can be invoked for (spending, minting, rewarding, certifying, plus the new voting and proposing purposes).

A script must pick one of these languages; the ledger supplies the matching context and enforces the matching set of allowed builtins.

Further reading