By Koz Ross, Plutarch maintainer at MLabs.
Introduction
Welcome to our first Plutarch Announcement! This is a new line of communication from the Plutarch team to Plutarch developers, and those interested in Plutarch more generally, tied to our quarterly release schedule. Here, we will discuss what is happening to Plutarch in this release, what this means for developers writing scripts in Plutarch, and what work we plan for the future. This is designed to supplement the changelogs provided as part of Plutarch, but go into more detail than we would normally be able to in a changelog format.
First, we will describe all the improvements and changes made to Plutarch's codebase. Second, we will discuss our work on Plutarch 2.0: a new, and significantly improved, form of Plutarch, addressing many of its issues in a way that the existing codebase could not support. Lastly, we will discuss future planned work, as well as the work of a new contributor to Plutarch.
All the changes we will discuss can be found on the tm1-q3-2026 branch. At the time of writing, these changes have been merged into master. Plutarch 2.0 is still a work in progress at this point, but you can see it [here][plutarch-2.0].
Priorities and choices
As per the funding we receive from the Treasury, we prioritized work as follows:
- Critical breakages and security issues have highest priority.
- Protocol-era and hardfork compatibility is next.
- Bug fixes, correctness improvements and optimizations are next.
- Developer experience, documentation and nice-to-haves of all other kinds have lowest priority.
To that end, our work on Plutarch primarily involved supporting the upcoming V4 ledger types and related builtin support. As part of this, we discovered an issue with PValidateData, which we have fixed and documented below. Plutarch 2.0 being a combination of optimizations, developer experience, documentation and many nice-to-haves, ended up being most of the rest of the work. A few additional small fixes were also included.
With regard to Ply, the upgrade to V4 created an issue with the interaction between Plinth's Blueprint, CIP-57 and how Ply uses both. This stems from the choice of V4's ledger types to use an encoding into List rather than Constr for those types that support it whenever possible. This is a change we welcome, and have supported for user-defined types in Plutarch for a long time. However, this creates a problematic interaction:
- Ply relies on a faithful representation of
Blueprint-indicated structure into a CIP-57 JSON representation. - CIP-57 currently does not retain the kind of element or length-related
information needed for the
List-encoded V4 ledger types to be faithfully marshalled and unmarshalled. - Thus, Ply cannot work correctly with many V4 ledger types as currently described.
Given Ply's less central role as a tool, as well as the complex interaction between all of these components, we decided to prioritize Plutarch over Ply in this release. Furthermore, due to the above, even upgrading Ply to support V4 ledger types is not possible without at least a significant redesign, if not an outright rewrite, of Ply at a fundamental level.
We believe Ply's functionality is important, and thus, we plan to integrate its capabilities into Plutarch 2.0 as part of the next milestone release. This will allow us to rethink the design and resolve the issues presented above, while also giving a much greater degree of integration between CIP-57 and Plutarch to developers who need it.
Improvements and fixes
Plutarch is now based on plutus-core version 1.71.0.0. This means that the newest V4 ledger API changes are fully supported, and plutarch-ledger-api and plutarch-orphanage have been appropriately adjusted. We have also made the following specific changes.
Additions
We have added support for the new primitive operations described in [CIP-156][cip-156] and [CIP-168][cip-168]. Now, Plutarch developers can use the following new operations, which mirror their similarly-named builtins:
pmultiIndexArrayppoliciespassetCountpkeepPoliciespdropPolicies
We have also added some missing instances:
PUnitandPNaturalnow havePValidateDatainstances; andPBuiltinValuenow has anPEqinstance.
The CIP-156 and CIP-168 operations are important for keeping up with the capabilities available to Plutus developers, while the omissions resolve missing functionality needed to support proper use of the types in question.
Fixes
As introduced in 1.12.0, generated PValidateData instances did not perform validation as thoroughly as they should have. To see why, let's consider a Data-encoded Plutarch version of a useful data type from [smash][smash]:
-- Assume generics-sop is imported with the qualifier `SOP` data PSmash (a :: S -> Type) (b :: S -> Type) (s :: S) =
PNada |
PSmash (Term s (PAsData a)) (Term s (PAsData b))
deriving stock (Generic)
deriving anyclass (SOP.Generic, PISData, PEq, PShow)
deriving (PlutusType, PValidateData) via (DeriveAsDataStruct PSmash)
This would generate a PValidateData instance equivalent to the following:
instance (PValidateData a, PValidateData b) => PValidateData (PSmash a b) where
pwithValidated d res = plet (pasConstr # d) $ \p ->
pmatch p $ \(PBuiltinPair tag fields) ->
punsafeCase tag [
-- Problem!
res,
pheadTailBuiltin fields $ \h1 t1 ->
-- Problem!
pwithValidated @a h1 (pwithValidated @b (pheadBuiltin # t1) res)
]
The issue stems from pwithValidated ignoring any 'trailing fields' provided in the Constr being checked. For example, all of these would validate given the above instance, even though only one is correct:
Constr 0 [](valid, representsPNada)Constr 0 [I 10](invalid, no fields forPNada)Constr 1 [I 10, B "foo", I 300](invalid, no third field forPSmash)
This is, of course, extremely problematic, as it enables 'padding' of datums in a way scripts do not expect.
This has now been remedied. The above definition for PSmash now generates a PValidateData that looks like this:
instance (PValidateData a, PValidateData b) => PValidateData (PSmash a b) where
pwithValidated d res = plet (pasConstr # d) $ \p ->
pmatch p $ \(PBuiltinPair tag fields) ->
punsafeCase tag [
doNada fields res,
doSmash fields res
]
where
doNada fields res = pmatch fields $ \case
PNil -> res
PCons _ _ -> perror
doSmash fields res = pheadTailBuiltin fields $ \h1 t1 ->
pwithValidated @a h1 (pheadTailBuiltin fields $ \h2 t2 ->
pwithValidated @b h2 (pmatch t2 $ \case
PNil -> res
PCons _ _ -> perror))
Now, any kind of 'padding' would cause pwithValidated to reject. With this change, we believe PValidateData is ready for use over the PTryFrom. Speaking of which...
Changes
PTryFrom, which has been a part of Plutarch for a long time, and been the main method of parsing arbitrary PData into more specific types, is being deprecated as of this release. There is a multitude of reasons for this, which we will list here. We decided not to take this step until now, as we were unsure about PValidateData's suitability as a replacement. However, at this stage, given the description above, we feel that it is time for PTryFrom to go.
So why replace, rather than try to fix, PTryFrom? Ultimately, our goals are, and have always been, usability with no performance compromises. PTryFrom, unfortunately, never really met these goals. Firstly, it is incredibly confusing to either read or write, which makes both usability and performance with it difficult. Secondly, PTryFrom tries to solve several different problems, which ultimately should not share a mechanism. Thirdly, any attempt to retro-fit PTryFrom to resolve either of these two issues would be more disruptive than a phased replacement. For these reasons, we decided to provide a substitute mechanism in PValidateData, with a transition period.
Given that PValidateData has been part of Plutarch for several releases now, PTryFrom is being deprecated, and will be removed in a future release. Here is what you can do in the meantime:
- If you were using an instance of the form
PTryFrom PData (PAsData Foo), use the correspondingPValidateDatainstance instead. If you need such an instance for your own type, you can derive it using the same mechanism as you use to derivePlutusType. - For any other instance, consider what checks you are performing, and use a different mechanism.
Plutarch 2.0
Plutarch's goal has always been performance. Thus, its initial design prioritized control by making use of advanced Haskell features. At the time of its design, this was the right choice; however, over the course of Plutarch's history, script developers wanted more high-level features mirroring those of Haskell. Additionally, our experience with maintaining Plutarch together with the changes and improvements in Plutus Core showed us that some of the decisions made when Plutarch was initially designed were holding us back.
More specifically, we identified the following issues:
- A lot of the features provided by Plutarch were hard for developers to understand. Both the motivation behind the design of these features, as well as how to properly use them, were obscure to all but the most familiar with both Haskell and UPLC.
- Plutarch's code generation was originally designed to be a thin wrapper around UPLC. With the changes in Plutus Core, as well as our improved understanding in what kind of compilation into UPLC was optimal, the code generation was patched in places to support higher-level optimizations. However, these were always poorly integrated, quite ad-hoc, and generally failed to interface well with the rest of the system.
- Plutarch had developed its own conventions and idioms, many of which stemmed from limitations in its original design, as well as requirements identified later both by the maintainers of Plutarch and Plutarch developers. These are not apparent to a newcomer, and makes the proper use of Plutarch much more difficult than it needs to be.
- Both in terms of its front and back end, Plutarch is remarkably opaque. Design choices for the embedding into the host language make GHC struggle with inference issues and error messages whenever something is done incorrectly, which makes the front-end obtuse, while at the back end, the only information is the final UPLC, which hides multiple intermediate stages and makes determining why such code was generated hard to impossible.
- Plutarch's internals rely on a large range of brittle mechanisms, many of
which are not to the standard of modern compiler design. The use of certain mechanisms (such as
generics-sop) practically guarantees brittleness on upgrades, opacity at all levels, and makes changes or contributions difficult and tedious. - Plutarch's developer experience has started to lag behind expectations from other languages that can be used to write scripts. Familiarity with UPLC is certainly helpful, but not in itself enough. In short, Plutarch is neither low-level enough, nor high-level enough, in its present form.
Addressing these issues within the scope of the current design of Plutarch, particularly its back-end code, is beyond the scope of what we could achieve without a clean break from the past. To this end, Plutarch 2.0 represents a full re-imagining and re-engineering of Plutarch, taking all of the lessons of its past into account, in combination with existing research, as well as our work on [Covenant][covenant]. With these changes, Plutarch 2.0 will be easier to write, easier to learn, more transparent, more efficient, and more fun.
Although our work is still ongoing, and Plutarch 2.0 cannot be used as a full replacement for the original Plutarch currently, we can demonstrate some of the improvements it brings. These are only highlights: many more possibilities remain, and will be described in full when Plutarch 2.0 has its full release.
See every stage
One of the frustrating aspects of Plutarch's original design is its opacity. It can be very difficult to see what's happening, or how something compiles. Furthermore, even though you can look at the UPLC Plutarch generates, you cannot really tell why any given code generation happens or not. In an environment as performance-sensitive as the chain, this is a complication that doesn't need to exist.
To this end, Plutarch 2.0 brings a new level of transparency to the process. Using built-in golden test functionality, given any closed Term, you can observe not just the final UPLC, but several intermediate stages of the Plutarch compiler's work as well. We have re-engineered the entire backend of Plutarch around this workflow, allowing for a new level of clarity. This helps not only Plutarch developers, but also the maintainers: being able to see what impacts any upgrade has on every stage of compilation clearly makes refactors or improvements much easier and faster.
To demonstrate the benefits of this transparency to Plutarch developers, let's consider a familiar function in original Plutarch:
pcountOld :: forall (a :: S -> Type) (s :: S) . (PLC.Contains PLC.DefaultUni (PlutusRepr a)) => Term s ((a :--> PBool) :--> PBuiltinList a :--> PInteger) pcountOld = plam $ \f -> pfix $ \self -> plam $ \ell -> pmatch ell $ \case PNil -> pconstant 0 PCons x xs -> pif (f # x) (pconstant 1 #+ self # xs) (self # xs)
If we lookat the UPLC that pcountOld compiles to, we get the following:
\i-0 -> (\i-1 -> i-1 i-1)
(\i-2 i-3 ->
case
i-3
[ (\i-4 i-5 ->
case (i-0 i-4) [(i-2 i-2 i-5), (addInteger 1 (i-2 i-2 i-5))])
, 0 ])
While reasonably readable (in light of earlier changes to Plutarch's prettyprinter for UPLC), there are numerous issues here. Firstly, several transformations made by the compiler are hidden from us here: both pif and PBuiltinList matches transform to cases, but this isn't easy to spot. Furthermore, there is a (deliberately introduced) issue in the implementation of pcountOld which duplicates the code applying to self. While we can spot this here, given how similar all variables look, in a larger or more complex expression, this becomes difficult or impossible.
Let's consider the same operation in Plutarch 2.0:
pcountNew :: forall (a :: S -> Type) (s :: S). (PlutarchType a) => Term s ((a :--> PBool) :--> PBList a :--> PInteger) pcountNew = plam1 $ \f -> pfix $ \self -> plam1 $ \ell -> pmatch ell $ \case PBNil -> pconstant 0 PBCons x xs ->
pif
(f # x)
(pconstant 1 #+ (self # xs))
(self # xs)
Plutarch 2.0 allows us multiple views of the intermediate stages of compilation. First, we can see a pretty representation of the RawTerm the Plutarch frontend builds:
\a -> fix (\b_self c ->
case c
[ \d e ->
case
(a # d)
[b_self # e, AddInteger # 1 :: Integer # (b_self # e)]
, 0 :: Integer
]
)
We can see that fixpoints are now given dedicated representations within RawTerm, which significantly helps readability. We can also see that pif translates to a UPLC case, and that the self argument of any fixpoint is clearly labelled as such. Lastly, we can also see that direct case on builtin lists is in use. All of this helps users clearly see 'surface-level' translations, such as how matching on PBList and functions like pif are implemented.
Furthermore, we can also view the next stage in the compilation process, which transforms the entire expression into [A-normal form][anf]:
ANF Hashes:
#0 := nlTHvWhSkgAN
#1 := hDbpCbz7Trb
#2 := m5XKL3leqWoN
#3 := vyRIuwDii3A
#4 := d0xRAemE28zN
#5 := ukQksefxmksN
#6 := eOiVhM4s41gN
#7 := mqE7eqJLvgvN
#8 := vHsG6EJRQry
#9 := btI5GD2KodEN
#10 := vuor2uyGdrp
#11 := hmaOEfDn3Sm
ANF Binds:
#0 := Apply a [d]
#1 := Apply b [e]
#2 := AddInteger
#3 := 1 :: Integer
#4 := Apply #2 [#3, #1]
#5 := Case #0 [#1, #4]
#6 := \d:1 e:1 -> #5
#7 := 0 :: Integer
#8 := Case c [#6, #7]
#9 := \c:1 -> #8
#10 := Fix[b:1] #9
#11 := \a:1 -> #10
This 'breaks down' more complex expressions into a sequence of primitive binds, which can be thought of as atomic units of work in UPLC. Each such bind gets a unique hash (which enables some optimizations we discuss later).
Continuing on, we can also see the dependency structure of the computation even more explicitly:
ANF Hashes:
#0 := nlTHvWhSkgAN
#1 := hDbpCbz7Trb
#2 := m5XKL3leqWoN
#3 := vyRIuwDii3A
#4 := d0xRAemE28zN
#5 := ukQksefxmksN
#6 := eOiVhM4s41gN
#7 := mqE7eqJLvgvN
#8 := vHsG6EJRQry
#9 := btI5GD2KodEN
#10 := vuor2uyGdrp
#11 := hmaOEfDn3Sm
ANF Binds:
#0 := Apply a [d]
#1 := Apply b [e]
#2 := AddInteger
#3 := 1 :: Integer
#4 := Apply #2 [#3, #1]
#5 := Case #0 [#1, #4]
#6 := \d:1 e:1 -> #5
#7 := 0 :: Integer
#8 := Case c [#6, #7]
#9 := \c:1 -> #8
#10 := Fix[b:1] #9
#11 := \a:1 -> #10
ANF Annotations:
#0 := No dependencies
#1 := No dependencies
#2 := No dependencies
#3 := No dependencies
#4 := Direct: {#1, #2, #3}, indirect: {}
#5 := Direct: {#0, #1, #4}, indirect: {#2, #3}
#6 := Direct: {#5}, indirect: {#0, #1, #2, #3, #4}
#7 := No dependencies
#8 := Direct: {#6, #7}, indirect: {#0, #1, #2, #3, #4, #5}
#9 := Direct: {#8}, indirect: {#0, #1, #2, #3, #4, #5, #6, #7}
#10 := Direct: {#9}, indirect: {#0, #1, #2, #3, #4, #5, #6, #7, #8}
#11 := Direct: {#10}, indirect: {#0, #1, #2, #3, #4, #5, #6, #7, #8, #9}
In this view, we can clearly see direct and indirect dependencies of each bind, which can help us understand which computations are needed where. This can help developers interested in smaller or faster code to see (and break) dependency chains, for example.
The final step before we generate UPLC is an additional analysis step for whether any given bind should be inlined or let-bound, and if so, where:
ANF Hashes:
#0 := nlTHvWhSkgAN
#1 := hDbpCbz7Trb
#2 := m5XKL3leqWoN
#3 := vyRIuwDii3A
#4 := d0xRAemE28zN
#5 := ukQksefxmksN
#6 := eOiVhM4s41gN
#7 := mqE7eqJLvgvN
#8 := vHsG6EJRQry
#9 := btI5GD2KodEN
#10 := vuor2uyGdrp
#11 := hmaOEfDn3Sm
ANF Binds:
#0 := Apply a [d]
#1 := Apply b [e]
#2 := AddInteger
#3 := 1 :: Integer
#4 := Apply #2 [#3, #1]
#5 := Case #0 [#1, #4]
#6 := \d:1 e:1 -> #5
#7 := 0 :: Integer
#8 := Case c [#6, #7]
#9 := \c:1 -> #8
#10 := Fix[b:1] #9
#11 := \a:1 -> #10
ANF Annotations:
#0 := Inlined, required let-binds here: {}
#1 := Let-bound, required let-binds here: {}
#2 := Inlined, required let-binds here: {}
#3 := Inlined, required let-binds here: {}
#4 := Inlined, required let-binds here: {}
#5 := Inlined, required let-binds here: {Rank 0 -> {#1}}
#6 := Inlined, required let-binds here: {}
#7 := Inlined, required let-binds here: {}
#8 := Inlined, required let-binds here: {}
#9 := Inlined, required let-binds here: {}
#10 := Inlined, required let-binds here: {}
#11 := Inlined, required let-binds here: {}
We can see that the analysis determined that bind #1 needs to be let-bound, as it is used twice, and the binding site should be at bind #5. #1 corresponds to the self argument application, while #5 is the pif checking if we should count the item. This immediately shows what issue we had in the original code - we would have evaluated the self call twice unless we used a plet.
Lastly, we can see the resulting UPLC:
\a -> let mArg_f = \b c ->
case c
[ \d e ->
(\bind_i ->
case
(a # d)
[bind_i, AddInteger # (1 :: Integer) # bind_i]
)
# (b # b # e)
, (0 :: Integer)
]
in mArg_f # mArg_f
While the output here is similar due to the way fixpoints have to be compiled, we can see the tell-tale sign of a let-binding transform at the heart of the code, which binds the self argument application. Unlike in the previous case, this happens without our input, automatically. We talk about this more in the subsequent section.
These changes help developers better understand what Plutarch does to the code they write, and shows them every step of the way in clear, manageable sections. In a setting as performance-sensitive as UPLC, this information is vital, and Plutarch 2.0 is designed to provide it from the get-go.
No more plet, no more phoistAcyclic
In Plutarch as it exists today, few other things are as confusing, or as critical, as plet and phoistAcyclic. Due to the level of control Plutarch gives to its developers, much of the responsibility for ensuring good code generation also falls to the developers. This can be powerful, but can just as often be frustrating and confusing. plet and phoistAcyclic demonstrate this dichotomy clearly.
As Plutarch is a code generator, a Plutarch Term is in fact a 'piece of code'. Thus, use of Haskell-level let will only name that particular 'piece of code', not ensure that it isn't duplicated. For example, consider the following function:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y -> let square = plam $ \z -> z #* z
in (square # x) #+ (square # y)
Someone not familiar with Plutarch would assume that the code for square would be generated once, to then be used twice with different arguments. However, this would not happen - all the above code does is name the chunk plam $ \z -> z #* z as square. What would end up compiled could just as well have been written as follows:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y ->
((plam $ \z -> z #* z) # x) #+ ((plam $ \z -> z #* z) # y)
To get the desired effect, a developer would instead have to write something like this:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y -> plet (plam $ \z -> z #* z) $ \square ->
(square # x) #+ (square # y)
The need to keep track of 'host language let-bind' versus 'target language let-bind' is not a requirement of Haskell, or indeed, any other language most developers are familiar with. This is particularly critical in Plutarch, as UPLC puts strict size requirements on code: thus, duplications like the one above are extremely undesirable, especially if they occur accidentally.
While this is already confusing enough, phoistAcyclic takes this problem and makes it worse. While having to remember about plet is annoying, plet's intent is relatively easy to understand from its signature. By contrast, phoistAcyclic's signature is positively cryptic, and its name is not much better:
phoistAcyclic :: forall (a :: S -> Type) (s :: S). HasCallStack => (forall (s' :: S). Term s' a) -> Term s a
To truly understand what this function does in terms of code generation, why you need it, or why it has the signature that it does, requires a fairly deep understanding of Plutarch's implementation choices, the precise role of the s argument, and even some familiarity with the peculiarities of how GHC compiles higher-rank polymorphism. This is not reasonable to expect of an onchain developer who is not also a compiler engineer, or a Haskeller who has dealt with GHC's rather peculiar error messages around skolems. The fact that this all combines to mean that the most optimal way to write sumOfSquares is as follows, with the signatures as given, is only apparent to a select few:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y -> (square # x) #+ (square # y)
where
square :: forall (s' :: S) . Term s (PInteger :--> PInteger)
square = phoistAcyclic $ plam $ \z -> z #* z
Arguably, better naming and documentation could help these problems. However, they are fundamentally not problems that ought to exist at all. With UPLC as a target, where code size is at a premium, Plutarch can, and should, automatically deduplicate equivalent terms. In fact, someone should be free to write
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y -> let square = plam $ \z -> z #* z
in (square # x) #+ (square # y)
or even
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam $ \x y ->
((plam $ \z -> z #* z) # x) #+ ((plam $ \z -> z #* z) # y)
and simply expect Plutarch to see the shared terms and deduplicate them automatically. This is not a new technique: it dates back [to the 1950s][hash-consing], and we have demonstrated its capabilities in Covenant, which performs this without any input from any developer.
For Plutarch 2.0, we wanted to bring similar capabilities, but found that we could do this even better than we did on Covenant. We achieved this by using a method similar to [this paper][hashing-modulo-alpha-equivalence], which allows more precise and efficient hash consing without having to 'embed' it into Plutarch's front-end. Indeed, if we define the equivalent to the above in Plutarch 2.0:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam2 $ \x y ->
let square = plam1 $ \z -> pmultiplyInteger # z # z
in (square # x) #+ (square # y)
then use the newly-described facilities above, we will see that the UPLC we generate looks right:
\a b -> let bind_h = \c -> MultiplyInteger # c # c in AddInteger # (bind_h # a) # (bind_h # b)
We could even write the 'worse' version:
sumOfSquares :: forall (s :: S) . Term s (PInteger :--> PInteger :--> PInteger) sumOfSquares = plam2 $ \x y -> ((plam1 $ \z -> pmultiplyInteger # z # z) # x) #+ ((plam1 $ \z -> pmultiplyInteger # z # z) # y)
and still get the right result:
\a b -> let bind_h = \c -> MultiplyInteger # c # c in AddInteger # (bind_h # a) # (bind_h # b)
In fact, we can see that the results produced are identical. This means that Plutarch 2.0 developers never have to think about deduplication of terms, no matter at what scale or where they reside. By a combination of the above-mentioned paper, and our intermediate ANF representation, we can perform this kind of deduplication across an entire script. This makes Plutarch 2.0 much easier and more fun to write: developers can focus on writing what they mean, and Plutarch 2.0 will make it fast.
Clearer derivations
Plutarch requires no small number of type classes to define behaviours for its types, both pre-existing and user-defined. Many of these type classes have extremely boilerplate definitions, which are fiddly, boring to write and should be derivable structurally. The original codebase provides a facility to achieve this, using generics-sop together with some deriving via helpers. While this works to reduce developer tedium, it brings with it several significant downsides:
- Any Plutarch project must depend on
generics-sopdirectly, purely to support derivations that ultimately have nothing to do with it. - It is extremely difficult to see what exactly gets generated and how. While it is possible to determine this for those who can read Core, the added lens of Plutarch makes this more difficult than it has any reason to be.
- Error messages stemming from accidental misuse of these derivations are cryptic and hard to resolve. This applies as much to Plutarch maintainers as to Plutarch users!
- Several methods are needed to derive everything. Some need
anyclassderivations, others needderiving via, and it's not clear which one should be used for what.
As an example of how tricky this can be, consider the PSmash example from earlier:
data PSmash (a :: S -> Type) (b :: S -> Type) (s :: S) =
PNada |
PSmash (Term s (PAsData a)) (Term s (PAsData b))
deriving stock (Generic)
deriving anyclass (SOP.Generic, PISData, PEq, PShow)
deriving (PlutusType, PValidateData) via (DeriveAsDataStruct PSmash)
The equivalent in Plutarch 2.0 is much more straightforward:
-- Enable TemplateHaskell data PSmash (a :: S -> Type) (b :: S -> Type) (s :: S) = PNada
| PSmash (Term s (PAsData a)) (Term s (PAsData b))
deriveFor ''PSmash DataPlutus NoEqDebug
Moreover, it is trivial to see what code gets generated: just put {-# OPTIONS_GHC -ddump-splices #-} at the top of the module, and recompile. This will produce the following in your terminal:
instance (PCanData a_al7z, PCanData b_al7A) =>
PlutarchType (PSmash a_al7z b_al7A) where
type PRepresentation (PSmash a_al7z b_al7A) = PData
instance (PCanData a_al7z, PCanData b_al7A) =>
PMatch (PSmash a_al7z b_al7A) where
pmatch' x_alfg f_alfh
= Plutarch.Primitive.Term.punsafeCase
x_alfg
(Data.Vector.NonEmpty.cons
(Plutarch.Backend.Term.toSomeTerm (plam' (\ _ -> f_alfh PNada)))
(Data.Vector.NonEmpty.singleton
(Plutarch.Backend.Term.toSomeTerm
(plam'
(\ x_alfi
-> Plutarch.Primitive.DerivedFun.pheadTailList
x_alfi
(plam'
(\ h_alfj
-> plam'
(\ t_alfk
-> f_alfh
(PSmash
(Plutarch.Primitive.Term.punsafeCoerce h_alfj)
(Plutarch.Primitive.Term.punsafeCoerce
(Plutarch.Primitive.BuiltinFun.pheadList
@PData
# t_alfk)))))))))))
instance (PCanData a_al7z, PCanData b_al7A) =>
PCon (PSmash a_al7z b_al7A) where
pcon' x_alfl
= case x_alfl of
PNada
-> ((Plutarch.Primitive.BuiltinFun.pconstrData
# Plutarch.Primitive.Term.punsafeConstant
(plutus-core-1.71.0.0-Lofrw56XKKuLvepsxrcB96:Universe.Core.someValue
@Integer 0))
# Plutarch.Primitive.BuiltinFun.pnilData)
PSmash f0_alfm f1_alfn
-> ((Plutarch.Primitive.BuiltinFun.pconstrData
# Plutarch.Primitive.Term.punsafeConstant
(plutus-core-1.71.0.0-Lofrw56XKKuLvepsxrcB96:Universe.Core.someValue
@Integer 1))
# ((Plutarch.Primitive.BuiltinFun.pmkCons # pfundamental f0_alfm)
# ((Plutarch.Primitive.BuiltinFun.pmkCons # pfundamental f1_alfn)
# Plutarch.Primitive.BuiltinFun.pnilData)))
instance (PCanData a_al7z, PCanData b_al7A) =>
PCanData (PSmash a_al7z b_al7A)
instance (PValidateData a_al7z, PValidateData b_al7A) =>
PValidateData (PSmash a_al7z b_al7A) where
pwithValidated d_alfo res_alfp
= (Plutarch.Primitive.Term.punsafeCase d_alfo
$ Data.Vector.NonEmpty.cons
(Plutarch.Backend.Term.toSomeTerm
(plam'
(\ x_alfv
-> (pmatch @(PBList PData) x_alfv
$ (\case
PBNil -> res_alfp
PBCons _ _ -> perror)))))
(Data.Vector.NonEmpty.singleton
(Plutarch.Backend.Term.toSomeTerm
(plam'
(\ x_alfq
-> Plutarch.Primitive.DerivedFun.pheadTailList
x_alfq
(plam'
(\ h_alfr
-> plam'
(\ t_alfs
-> pwithValidated
@a_al7z h_alfr
(Plutarch.Primitive.DerivedFun.pheadTailList
t_alfs
(plam'
(\ h_alft
-> plam'
(\ t_alfu
-> pwithValidated
@b_al7A h_alft
(pmatch
@(PBList PData) t_alfu
$ (\case
PBNil -> res_alfp
PBCons _ _
-> perror))))))))))))))
While this is not the most readable code, it is at least familiar Haskell. Moreover, what you see is exactly what you get: this is the exact code that will be sent to GHC for compilation. If you wanted to improve or modify it, use it as a basis for your own implementation for your type, or just better understand what happens, you can do so easily. This makes both learning and using Plutarch 2.0 much easier.
Furthermore, the new mechanism requires much less typing and remembering of which derivation does what: everything is done using a single line, specifying a Strategy (which determines representation) and whether you want all instances or for some to be skipped (as in the above example for brevity). This is again a big improvement, as developers who don't want to understand all of Plutarch's specifics now do not have to.
Lastly, even if the mechanism is not used appropriately, the errors it will emit are much easier to diagnose. As there is no 'intermediate layer' like generics-sop possibly introducing its own issues, everything can be diagnosed by looking at either the call to deriveFor, or at the generated code. This means that familiarity with both Haskell and Plutarch (to a lesser extent) is all you need to diagnose any issues stemming from this new mechanism.
Parallel let
One important way of ensuring that Plutarch scripts are as small as possible is to ensure we never duplicate computations. The plet mechanism in the original Plutarch was designed to allow this. The way plet worked is by transforming
plet foo $ \bar -> f bar
into
(plam $ \bar -> f bar) # foo
This is a familiar transformation from other functional languages.
When we consider multiple, consecutive plets in this compilation model, we can see that for each plet, we have to compile:
- A lambda; and
- An application.
Furthermore, plet is 'sequential': 'earlier' plet-bound variables can be used in 'later' plets:
-- Works fine plet foo $ \bar ->
plet (f bar) $ \baz ->
g baz
This approach is simple and reliable, and has worked well since Plutarch's beginnings. However, in some cases, we can do better. If we don't need to plet 'sequentially':
plet (foo #+ 3) $ \bar ->
plet (foo #+ 1) $ \baz ->
f bar baz
we can make use of constr and case instead:
case (constr 0 [foo #+ 3, foo #+ 1]) [
\bar baz -> f bar baz
]
This is only possible here because bar and baz don't depend on each other. In this 'parallel' plet, our costs change:
- One
constrandcasefor allplets - One lambda (technically) for each
plet
This, combined with the low cost of a UPLC case expression, potentially saves both code size and performance. This optimization is already used by Aiken: Plutarch 2.0 not only makes more thorough use of it, but also does so automatically.
Future work
For those who are worried that Plutarch 2.0 will completely displace the current Plutarch codebase, fear not. We have no plans to stop supporting Plutarch's current codebase at the moment. At the very minimum, the funding provided by the Treasury will ensure support for another three quarters, wherein the funding will be split between the old codebase and Plutarch 2.0.
Our priority for the next milestone is making Plutarch 2.0 fully usable. This will include support for the ledger API, full documentation for developers building on Plutarch 2.0, and additional performance improvements and tests. Another important step will be the integration of Ply functionality into Plutarch 2.0, which will simplify CIP-57 support for Plutarch developers.
We also want to welcome our newest contributor, Sandeep, who is joining the Plutarch project as part of the [Maintainer Retainer Program][maintainer-retainer]. Sandeep is working on an important component to the future of Plutarch 2.0: an evaluator for UPLC that does not depend on the rest of the Plutus codebase. This would allow Plutarch 2.0 to become a standalone Haskell project, needing no dependencies on either Plutus or Nix. This would also give us additional evaluation control where we need it, providing more power for both tests and optimizations in Plutarch 2.0. The work is still in its early stages, but you can find it at [this repository][sandeep].
Video walkthroughs
- Walkthrough of the changes to the original repository
- Walkthrough of the new repository (Plutarch 2.0)
- Tests running in the original repository
- Tests running in the new repository
[cip-156]: https://cips.cardano.org/cip/CIP-0156 [cip-168]: https://cips.cardano.org/cip/CIP-0168 [smash]: https://flora.pm/packages/@hackage/smash [covenant]: https://projectcatalyst.io/funds/13/cardano-open-developers/mlabs-static-analysis-with-covenant [hash-consing]: https://dl.acm.org/doi/10.1145/368892.368907 [hashing-modulo-alpha-equivalence]: https://arxiv.org/pdf/2105.02856 [anf]: https://en.wikipedia.org/wiki/A-normal_form [maintainer-retainer]: https://opensourcecommittee.docs.intersectmbo.org/about/paid-open-source-model-posm/maintainer-retainer [sandeep]: https://github.com/sandykwl/cardano-uplc [plutarch-2.0]: https://github.com/mlabs-haskell/plutarch