LambdaBuffers for Plutarch

Plutarch is a typed eDSL in Haskell for writing efficient Plutus Core validators.

LambdaBuffers creates Plutarch type definitions and associated Plutarch type class implementations for PlutusType, PIsData and PShow classes.

Additionally, when instructed by a LambdaBuffers derive statement type class implementations for PEq and PTryFrom are also printed.

A small example:

❯ nix develop github:mlabs-haskell/lambda-buffers#dev-plutarch
❯ cat > Example.lbf
module Example

import Prelude
import Plutus.V1 (PlutusData, AssetClass)

record Example a = {
  foo : AssetClass,
  bar : a
  }

derive Eq (Example a)
derive Json (Example a)
derive PlutusData (Example a)

❯ lbf-plutus-to-plutarch Example.lbf
[lbf][INFO] Compilation OK
[lbf][INFO] Codegen OK

❯ find autogen/
autogen/
autogen/build.json
autogen/LambdaBuffers
autogen/LambdaBuffers/Example
autogen/LambdaBuffers/Example/Plutarch.hs

For a full example see Example.

LambdaBuffers modules

Writing .lbf schemas with API types intended for Plutarch backend will typically use the following LambdaBuffers schema modules:

  1. Prelude,
  2. Plutus.V1,
  3. Plutus.V2.

Take a look at Example.lbf schema as an example.

Haskell libraries

The necessary LambdaBuffers runtime libraries a typical Plutarch project needs when working with LambdaBuffers:

  1. lbr-plutarch a Haskell runtime library necessary for working with lbf-xyz libraries.
  2. lbf-prelude-plutarch that contains the LambdaBuffers Prelude schema library generated by LambdaBuffers.
  3. lbf-plutus-plutarch that contains the LambdaBuffers Plutus schema library generated by LambdaBuffers.

Of course, additional imports for Plutarch libraries are also necessary plutarch and optionally plutarch-extra.

For a full example see Example.

Inspecting the generated output

You can inspect the generated libraries using Nix:

❯ nix build .#lbf-prelude-plutarch
❯ find result/autogen/
result/autogen/
result/autogen/LambdaBuffers
result/autogen/LambdaBuffers/Prelude
result/autogen/LambdaBuffers/Prelude/Plutarch.hs

❯ nix build .#lbf-plutus-plutarch
❯ find result/autogen/
result/autogen/
result/autogen/LambdaBuffers
result/autogen/LambdaBuffers/Plutus
result/autogen/LambdaBuffers/Plutus/V2
result/autogen/LambdaBuffers/Plutus/V2/Plutarch.hs
result/autogen/LambdaBuffers/Plutus/V1
result/autogen/LambdaBuffers/Plutus/V1/Plutarch.hs

Haskell modules

The set of imports a Plutarch program using LambdaBuffers would typically need is the following:

import LambdaBuffers.Plutus.V1.Plutarch ()
import LambdaBuffers.Plutus.V2.Plutarch ()
import LambdaBuffers.Prelude.Plutarch ()
import LambdaBuffers.Runtime.Plutarch ()
import Plutarch ()
import Plutarch.Prelude ()
import Plutarch.Api.V1 ()
import Plutarch.Api.V2 ()
  1. LambdaBuffers.Plutus.V1.Plutarch is a module generated from Plutus.V1 LambdaBuffers schema and provided by the lbf-plutus-plutarch runtime library.
  2. LambdaBuffers.Plutus.V2.Plutarch is a module generated from Plutus.V2 LambdaBuffers schema and provided by the lbf-plutus-plutarch runtime library.
  3. LambdaBuffers.Prelude.Plutarch is a module generated from Prelude LambdaBuffers schema and provided by the lbf-prelude-plutarch runtime library.
  4. LambdaBuffers.Runtime.Plutarch is a module provided by the lbr-plutarch runtime library.

Generated Plutarch module for a LambdaBuffers schema Foo/Bar.lbf (ie. Foo.Bar) is stored at Foo/Bar/Plutarch.hs

Restrictions

Plutarch backend doesn't support recursive type definitions unfortunately (see #131).

The following will not work:

module ModuleWithRecursiveType

import Prelude (Eq)
import Plutus.V1 (PlutusData)

sum List a = Cons a (List a) | Nil
derive Eq (List a)
derive PlutusData (List a)

Additionally, LambdaBuffers record types are mapped to Plutarch product types:

module ModuleWithARecordType

import Prelude (Eq, Integer, Bool)
import Plutus.V1 (PlutusData)

record Foo = {
  bar: Integer,
  baz: Bool
  }
derive Eq Foo
derive PlutusData Foo

Essentially, the record definitions are 'degraded' into product types such that the order of product fields is the order of record fields as they are defined at source.

For example the Foo record defined above would have no difference in Plutarch if it was defined as product Foo below:

prod Foo = Integer Bool

The Plutarch backend doesn't support the use of Char, Text, Bytes (there's a Plutus.V1.Bytes), Set and Map (there's a Plutus.V1.Map) from LambdaBuffers Prelude module.

Plutarch

Type definition mapping

Plutarch backend supports all types from the LambdaBuffers Plutus schema library, as to enable full featured Plutus script development.

Additionally, it also supports some types from the LambdaBuffers Prelude schema library, namely Bool, Integer, Maybe, Either and List.

module Foo

sum Sum = Some a | Nothing

record Record a = {
  foo : Bytes,
  bar: a
}

prod Product a = Bytes a

translates into Plutarch equivalent:

module LambdaBuffers.Foo.Plutarch (Sum(..), Record(..), Product(..)) where

import qualified LambdaBuffers.Plutus.V1.Plutarch
import qualified LambdaBuffers.Prelude.Plutarch
import qualified LambdaBuffers.Runtime.Plutarch
import qualified Plutarch
import qualified Plutarch.Bool
import qualified Plutarch.Builtin
import qualified Plutarch.Internal.PlutusType
import qualified Plutarch.Prelude
import qualified Plutarch.Show
import qualified Plutarch.TryFrom
import qualified Plutarch.Unsafe

data Sum (a :: PType) (s :: Plutarch.S) = Sum'Some (Plutarch.Term s (Plutarch.Builtin.PAsData LambdaBuffers.Plutus.V1.Plutarch.Bytes)) (Plutarch.Term s (Plutarch.Builtin.PAsData PAsData))
                                  | Sum'Nothing
  deriving stock GHC.Generics.Generic
  deriving anyclass Plutarch.Show.PShow

data Record (a :: PType) (s :: Plutarch.S) = Record (Plutarch.Term s (Plutarch.Builtin.PAsData LambdaBuffers.Plutus.V1.Plutarch.Bytes)) (Plutarch.Term s (Plutarch.Builtin.PAsData PAsData))
  deriving stock GHC.Generics.Generic
  deriving anyclass Plutarch.Show.PShow

data Product (a :: PType) (s :: Plutarch.S) = Product (Plutarch.Term s (Plutarch.Builtin.PAsData LambdaBuffers.Plutus.V1.Plutarch.Bytes)) (Plutarch.Term s (Plutarch.Builtin.PAsData PAsData))
  deriving stock GHC.Generics.Generic
  deriving anyclass Plutarch.Show.PShow

Type class implementations

Plutarch has a couple of fundamental type classes essential to its operations namely, PlutusType, PIsData, PTryFrom and PEq.

PlutusType

Printing an implementation for this class for a particular type is governed by derive Plutus.V1.PlutusData <type> statements in .lbf schemas.

PlutusType serves to (de)construct Plutarch eDSL terms from Haskell 'native' terms.

class PlutusType (a :: PType) where
  type PInner a :: PType
  pcon' :: forall s. a s -> Term s (PInner a)
  pmatch' :: forall s b. Term s (PInner a) -> (a s -> Term s b) -> Term s b

Additionally, Plutarch enables specifying terms to have different 'value' representation, like Scott encoded terms or PlutusData encoded terms. This is what the PInner type family is used to specify. LambdaBuffers only cares about PlutusData encoded terms since we're using it to specify Plutus datum structures.

The task is to generate a pcon' implementation such that we can construct Plutarch Terms that have some PInner representation of type PData, from Haskell 'native' values. The pcon' implementation must match the LB Plutus PlutusData encoding class standard, and so we'll use the same 'to Plutus data' specification to generate pcon' implementations.

Constructing is always only one part of the story, there's also deconstruction that is captured by the pmatch' method. This method serves to 'pattern match' on a value that was already constructed using pcon' and dispatch said value to a provided continuation function. It's important to note that there's a subtle but important distinction to be made between the ptryFrom and pmatch' methods. pmatch' assumes that the value it receives is indeed correct, as it was constructed using the pcon' method. This means that pmatch' should never error, and if it does that means the implementation is wrong. ptryFrom is different, as it takes some PData and tries to parse it into a PType, but can fail.

However, in LambdaBuffers, both of these methods follow the exact same logical pattern, and they correspond and can be generated using the from Plutus data specification.

PTryFrom

Printing an implementation for this class for a particular type is governed by derive Plutus.V1.PlutusData <type> statements in .lbf schemas.

PTryFrom serves specify how PData is 'parsed' into a Plutarch type. N It's generally used to convert between Plutarch types, but that's a fairly general use case, and we generally use this class in a very narrow form to specify how PData is 'parsed' into a Plutarch type.

class PSubtype a b => PTryFrom (a :: PType) (b :: PType) where
  type PTryFromExcess a b :: PType
  type PTryFromExcess a b = PTryFromExcess a (PInner b)
  ptryFrom' :: forall s r. Term s a -> ((Term s b, Reduce (PTryFromExcess a b s)) -> Term s r) -> Term s r
  default ptryFrom' :: forall s r. (PTryFrom a (PInner b), PTryFromExcess a b ~ PTryFromExcess a (PInner b)) => Term s a -> ((Term s b, Reduce (PTryFromExcess a b s)) -> Term s r) -> Term s r
  ptryFrom' opq f = ptryFrom @(PInner b) @a opq \(inn, exc) -> f (punsafeCoerce inn, exc)

There's some additionally features exhibited by this type class, most noteworthy is the PTryFromExcess type family that enables us specify the part of the structure that wasn't parsed and is left unexamined. It's a form of optimization that becomes very important if you have a very complex data type such as ScriptContext from the plutus-ledger-api.

Apparently, a good intuition pump for this 'excess' business is that of a zipper. We focus on a certain part of a data structure, only ever providing links to other parts that are left un-examined.

LambdaBuffers doesn't use this feature and sets the PTryFromExcess to a unit type, signaling that nothing is left unexamined.

PIsData

Printing an implementation for this class for a particular type is governed by derive Plutus.V1.PlutusData <type> statements in .lbf schemas.

PIsData serves to track 'is it Plutus data encoded?' with types.

newtype PAsData (a :: PType) (s :: S) = PAsData (Term s a)

class PIsData a where
  pfromDataImpl :: Term s (PAsData a) -> Term s a
  default pfromDataImpl :: PIsData (PInner a) => Term s (PAsData a) -> Term s a
  pfromDataImpl x = punsafeDowncast $ pfromDataImpl (punsafeCoerce x :: Term _ (PAsData (PInner a)))

  pdataImpl :: Term s a -> Term s PData
  default pdataImpl :: PIsData (PInner a) => Term s a -> Term s PData
  pdataImpl x = pdataImpl $ pto x
instance PIsData FooTrivial where
  pdataImpl = punsafeCoerce
  pfromDataImpl = punsafeCoerce

instance PEq FooTrivial where
  (#==) = \l r -> pdata l #== pdata r

Due to generated types having a PAsData attached to them, be ready to use pdata and pfromData to switch between forms.

PEq

Printing an implementation for this class for a particular type is governed by derive Prelude.Eq <type> statements in .lbf schemas.

PEq serves to track provide equality checks to Plutarch types.

class PEq t where
  (#==) :: Term s t -> Term s t -> Term s PBool
  default (#==) ::
    (PGeneric t, PlutusType t, All2 PEq (PCode t)) =>
    Term s t ->
    Term s t ->
    Term s PBool
  a #== b = gpeq # a # b

infix 4 #==

We don't generate an implementation from the LambdaBuffers 'equality spec', rather we delegate the equality check to the underlying 'PData' representations that all generated types have for performance.

PShow

All generated types have a PShow instance derived using the internal Plutarch deriving mechanism.

PShow serves to stringify Plutarch types which is very useful during debugging.

Example

Let work through the Plutarch example available in the repo.

First, please check the Getting started guide on how to prepare to work with the repo and setup Nix.

Let's see what we have here:

lambda-buffers/docs/plutarch ❯ find
.
./build.nix
./cabal.project
./hie.yaml
./plutarch-example.cabal
./app
./app/Example.hs
./api
./api/Example.lbf
./.envrc

The salient bits we should focus on are:

  1. The LambdaBuffers .lbf schema in ./api/Example.lbf that describes the API types used by our little program,
  2. The Haskell Plutarch program in ./app/Example.hs that works with the API types.

To inspect the generated library:

lambda-buffers/docs/plutarch ❯ nix build .#lbf-plutarch-example-api
lambda-buffers/docs/plutarch ❯ find autogen/
autogen/
autogen/build.json
autogen/LambdaBuffers
autogen/LambdaBuffers/Example
autogen/LambdaBuffers/Example/Plutarch.hs

The name of the generated library lbf-plutarch-example-api is set in the ./plutarch/build.nix Nix build file.

However, it's not expected for users to need to do this. If you have any issue please reach out.

Inspecting the Cabal file shows the standard runtime libraries we need:

lambda-buffers/docs/plutarch ❯ cabal info .
* plutarch-example-0.1.0.0 (program)
    Synopsis:      LambdaBuffers Plutarch example
    Versions available: [ Not available from server ]
    Versions installed: [ Unknown ]
    Homepage:      [ Not specified ]
    Bug reports:   [ Not specified ]
    License:       NONE
    Author:        Drazen Popovic
    Maintainer:    bladyjoker@gmail.com
    Source repo:   [ Not specified ]
    Executables:   plutarch-example
    Flags:         dev
    Dependencies:  base >=4.16, lbf-plutarch-example-api, lbf-plutus-plutarch,
                   lbf-prelude-plutarch, lbr-plutarch, plutarch, plutarch-extra,
                   text >=1.2
    Cached:        Yes

Run the program:

lambda-buffers/docs/plutarch ❯ cabal run
"Friends, peace and love!!!"

Take a look at the Example.hs to see how generated types are used, namely how they are constructed with pcon and deconstructed with pmatch (or pmatchC).