Skip to content

Commit a08c402

Browse files
authored
Add plutus-tx tests for compiling to case on built-ins (#7539)
1 parent 91fc693 commit a08c402

File tree

9 files changed

+192
-77
lines changed

9 files changed

+192
-77
lines changed

doc/docusaurus/docs/delve-deeper/casing-constants.md

Lines changed: 139 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,32 @@ This use of `case` in UPLC was introduced with protocol version 11 and requires
1313
:::
1414

1515
In UPLC, it is possible to branch on expressions of certain built-in types, like
16-
`Integer` and `Bool`. This can be done with `case` syntax, which is also used
17-
for [sums-of-products](./encoding#sums-of-products). Using `case` on built-in
18-
values may improve script performance and size.
19-
The built-in types that `case` currently supports are:
16+
`Integer` and `Bool` using `case` syntax, which is also used
17+
for [sums-of-products](./encoding#sums-of-products). This may improve script performance
18+
and size, compared to older alternatives that use built-in functions.
19+
20+
This page describes the built-in types that are supported in UPLC and how Plinth
21+
developers can benefit from the improved performance.
22+
23+
## Usage from Plinth
24+
25+
:::info
26+
27+
The Plinth compiler option will change from `datatypes=BuiltinCasing` to
28+
`datatypes=SumsOfProducts` in the future.
29+
30+
:::
31+
32+
33+
Plinth developers can benefit from the performance of `case` by enabling the
34+
compiler [option](./plinth-compiler-options) `datatypes=BuiltinCasing`, and
35+
using standard library functions such as `caseList`, `fstPair`.
36+
Note that Plinth's `case ... of ...` syntax is not generally compiled to UPLC,
37+
as it can be more expressive.
38+
39+
## Supported types
40+
41+
The UPLC built-in types that `case` can be used on are:
2042

2143
- `bool`
2244
- `unit`
@@ -26,144 +48,184 @@ The built-in types that `case` currently supports are:
2648

2749
In the future, support for `data` is also planned.
2850

29-
For each type, the allowed branches and their order differs. See [Supported
30-
Types](#supported-types) for more detail.
31-
32-
## Compiling to `case` in Plinth
33-
34-
When compiling Plinth code with the [option](./plinth-compiler-options)
35-
`datatypes=BuiltinCasing` (which in the future be achieved with
36-
`datatypes=SumsOfProducts), many standard library functions will be compiled
37-
into this use of `case`, such as `fstPair`, `ifThenElse` and `caseList`. Note
38-
that Plinth's `case ... of ...` syntax is not necessarily compiled to UPLC, as
39-
it can be more expressive.
40-
41-
## Supported types
42-
4351
### Bool
4452

45-
Booleans can be used in `case` with either one or two branches, where the first
46-
is the `False` branch. Boolean negation can be written for example as:
53+
The following Plinth code implements a basic assertion:
4754

48-
```uplc
49-
lam b (case b (con bool True) (con bool False))
55+
```haskell
56+
assert :: Bool -> BuiltinUnit
57+
assert False = error ()
58+
assert True = unitval
5059
```
5160

52-
When only a single branch is provided, script execution will fail when the
53-
boolean evaluates to `True`.
61+
With `datatypes=BuiltinCasing`, it is compiled to the new casing on builtins:
5462

55-
Using a single branch is appropriate when the second branch was supposed to fail
56-
already, saving script size.
63+
```uplc
64+
\b -> case b [error, ()]
65+
```
5766

67+
In UPLC, booleans can be used in `case` with either one or two branches, where
68+
the first is always the `False` branch. When only a single branch is provided,
69+
script execution will fail when the boolean evaluates to True.
5870

5971
:::info
6072

61-
When compiling without `datatypes=BuiltinCasing`, Plinth's `ifThenElse` is
62-
compiled into UPLC's `ifThenElse` built-in function, which usually requires more
63-
AST nodes since each branch argument needs to be delayed (function application is
64-
strict), and finally force the chosen branch. This impacts the size and
65-
execution cost.
73+
Compare this to the UPLC that would have been generated otherwise:
74+
75+
```uplc
76+
\b -> force (force ifThenElse b (delay ()) (delay error))
77+
```
6678

79+
This uses the UPLC builtin function `ifThenElse`, which requires delaying the branch
80+
arguments, since application in UPLC is strict. The additional forcing and
81+
delaying impacts the size and execution cost.
6782
:::
6883

6984

70-
### Unit
85+
### BuiltinUnit
86+
87+
The built-in unit type can be used in a trivial way with `case` in UPLC, which
88+
takes exactly one branch. With `datatypes=BuiltinCasing`, Plinth will compile
89+
`chooseUnit` from `PlutusTx.Builtins.Internal` into `case`. For example:
90+
91+
```haskell
92+
forceUnit :: BuiltinUnit -> Integer
93+
forceUnit e = chooseUnit e 5
94+
```
7195

72-
Needs exactly one branch. If the expression being cased on evaluates to a unit
73-
value, evaluation will continue with the expression in that branch. For example,
74-
this expression evaluates to `con integer 5`.
96+
Which results in the following UPLC:
7597

7698
```uplc
77-
case (con unit ()) (con integer 5)
99+
\e -> case e [5]
78100
```
79101

80-
### Pair
102+
UPLC's case on built-in unit requires exactly one branch. If the expression
103+
being cased on evaluates to the unit value, evaluation will continue with the
104+
expression in that branch.
81105

82-
A built-in pair expects a single branch: a function that takes both components
83-
of the pair.
106+
### BuiltinPair
84107

85-
This example sums the two integer constants in a pair.
108+
To destruct a built-in pair, use `casePair` from `PlutusTx.Builtins`. For example:
86109

87-
```uplc
88-
lam x (case x (lam a (lam b [(builtin addInteger) a b])))
110+
```haskell
111+
addPair :: BuiltinPair Integer Integer -> Integer
112+
addPair p = casePair p (+)
113+
```
114+
115+
This compiles into `case` in UPLC, which expects a single branch:
116+
117+
```
118+
\p -> case p [(\x y -> addInteger x y)]
89119
```
90120

91121
:::info
92122

93123
When compiling without `datatypes=BuiltinCasing`, Plinth's `choosePair` is
94124
compiled into multiple built-in function calls to project out the pair's
95-
components, impacting size and execution cost.
125+
components, impacting size and execution cost:
126+
127+
```uplc
128+
\p -> addInteger (force (force fstPair) p) (force (force sndPair) p)
129+
```
96130

97131
:::
98132

99133
### Integer
100134

101-
Casing on integers can be used for non-negative integers only, and a variable
135+
Casing on integers in UPLC can be used for non-negative integers only, and a variable
102136
amount of branches may be given. If the expression `e` evaluates to an integer
103-
`i`, the `i`th branch will be evaluated. If there is no branch, `case` will fail.
137+
`i`, the `i`th branch will be evaluated. If there is no branch, `case` will
138+
fail.
104139

105-
For example, the following expression evaluates to `con string "c"`
140+
In Plinth, use the `caseInteger` function:
106141

107-
```
108-
case [(builtin addInteger) (con integer 1) (con integer 1)]
109-
(con string "a")
110-
(con string "b")
111-
(con string "c")
142+
```haskell
143+
integerABC :: Integer -> BuiltinString
144+
integerABC i = caseInteger i ["a", "b", "c"]
112145
```
113146

114-
If the `i`th branch is not given, or `i` is a negative integer, evaluation will
115-
fail:
147+
Applying this function to `2` gives `"c"`, while `10` or `-1` produce an error.
148+
Note that the second argument must be given as a literal list, otherwise it is a
149+
Plinth compile error.
116150

117-
```
118-
case [(builtin addInteger) (con integer 2) (con integer 2)]
119-
(con string "a")
120-
(con string "b")
121-
(con string "c")
122-
```
123151

124-
Results in
152+
Plinth generates the following UPLC:
125153

126154
```
127-
An error has occurred:
128-
'case' over a value of a built-in type failed with
129-
'case 4' is out of bounds for the given number of branches: 3
130-
Caused by: 4
155+
\i -> case i ["a", "b", "c"]
131156
```
132157

133-
Note that there is no way to provide a "catch-all" case for integers.
158+
In general, if `i`th branch is not given, or `i` is a negative integer, evaluation will
159+
fail. Note that there is no way to provide a "catch-all" case for integers.
134160

135161
:::info
136162

137-
In Plinth, using `caseInteger` with `datatypes=BuiltinCasing` will be compiled into
138-
the above `case` construct in PIR, provided the second argument is given as a
139-
literal list (otherwise this is a compile error).
140-
141163
When not using `datatypes=BuiltinCasing`, Plinth's `caseInteger` is compiled
142164
into a much less efficient implementation that turns the second argument in a
143165
list (of which the representation depends on the chosen `datatypes=` flag), and
144-
does a recursive lookup in that list.
166+
does a recursive lookup in that list. The above Plinth code is compiled to:
167+
145168

169+
```uplc
170+
(\traceError ->
171+
(\go i ->
172+
force
173+
(force ifThenElse
174+
(lessThanInteger i 0)
175+
(delay (traceError "PT6"))
176+
(delay
177+
(go
178+
i
179+
(constr 1
180+
[ "a"
181+
, (constr 1
182+
["b", (constr 1 ["c", (constr 0 [])])]) ])))))
183+
((\s -> s s)
184+
(\s ds ds ->
185+
case
186+
ds
187+
[ (traceError "PT7")
188+
, (\x xs ->
189+
force
190+
(force ifThenElse
191+
(equalsInteger 0 ds)
192+
(delay x)
193+
(delay
194+
((\x -> s s x) (subtractInteger ds 1) xs)))) ])))
195+
(\str -> (\x -> error) (force trace str (constr 0 [])))
196+
```
146197
:::
147198

148199

149-
### List
200+
### BuiltinList
150201

151202
A `case` on built-in lists may be given one or two branches (similar to
152203
booleans), where the first one deals with the cons case, and the second one with
153204
the empty list. If no second branch is given, execution will fail when the list
154205
turns out to be empty.
155206

156-
This example implements the `head` function, which fails if the list if empty.
207+
This example implements a `head` function for boolean lists, which fails if the list if empty.
157208

158209
```uplc
159-
lam xs (case xs (lam y (lam ys y)))
210+
head :: BuiltinList Bool -> Bool
211+
head xs = caseList (\_ -> error ()) (\x _ -> x) xs
160212
```
161213

162214
:::info
163215

164-
When compiling without `datatypes=BuiltinCasing`, Plinth's `caseList` is
165-
compiled into a combination of built-in calls such as `chooseList`, `headList`
166-
and `tailList`. Similarly to booleans, the branches are also thunked, impacting
167-
script size and execution cost.
216+
When compiling without `datatypes=BuiltinCasing`, compilation falls back on
217+
using multiple built-ins, such as `chooseList`, `headList` and `tailList`.
218+
Similarly to booleans, the branches are thunked, impacting script size and
219+
execution cost:
220+
221+
```uplc
222+
(\xs ->
223+
force
224+
(force (force chooseList)
225+
xs
226+
(delay (\ds -> error))
227+
(delay ((\x xs ds -> x) (force headList xs) (force tailList xs))))
228+
(constr 0 []))
229+
```
168230

169231
:::

plutus-tx-plugin/plutus-tx-plugin.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ test-suite plutus-tx-plugin-tests
131131
Budget.Spec
132132
Budget.WithGHCOptimisations
133133
Budget.WithoutGHCOptimisations
134+
BuiltinCasing.Spec
134135
BuiltinList.Budget.Spec
135136
BuiltinList.NoCasing.Spec
136137
ByteStringLiterals.Lib
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(program 1.1.0 (\p -> case p [(\x y -> addInteger x y)]))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(program 1.1.0 (\ds -> case ds [error, ()]))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(program 1.1.0 (\e -> case e [5]))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(program 1.1.0 (\xs -> case xs [(\x xs ds -> x), (\ds -> error)] (constr 0 [])))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
(program 1.1.0 (\i -> case i ["a", "b", "c"]))
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{-# LANGUAGE DataKinds #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE TemplateHaskell #-}
4+
{-# LANGUAGE NoImplicitPrelude #-}
5+
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:datatypes=BuiltinCasing #-}
6+
7+
{-| This module tests compilation of built-in types into UPLC's casing of constants
8+
the examples also appear in the Plinth guide page about case -}
9+
module BuiltinCasing.Spec where
10+
11+
import Test.Tasty.Extras
12+
13+
import PlutusTx (compile)
14+
import PlutusTx.Builtins (caseInteger, caseList, casePair)
15+
import PlutusTx.Builtins.Internal (chooseUnit, unitval)
16+
import PlutusTx.Prelude
17+
import PlutusTx.Test
18+
19+
assert :: Bool -> BuiltinUnit
20+
assert False = error ()
21+
assert True = unitval
22+
23+
forceUnit :: BuiltinUnit -> Integer
24+
forceUnit e = chooseUnit e 5
25+
26+
addPair :: BuiltinPair Integer Integer -> Integer
27+
addPair p = casePair p (+)
28+
29+
integerABC :: Integer -> BuiltinString
30+
integerABC i = caseInteger i ["a", "b", "c"]
31+
32+
head :: BuiltinList Bool -> Bool
33+
head xs = caseList (\_ -> error ()) (\x _ -> x) xs
34+
35+
tests :: TestNested
36+
tests =
37+
testNested "BuiltinCasing"
38+
. pure
39+
$ testNestedGhc
40+
[ goldenUPlcReadable "assert" $$(compile [||assert||])
41+
, goldenUPlcReadable "forceUnit" $$(compile [||forceUnit||])
42+
, goldenUPlcReadable "addPair" $$(compile [||addPair||])
43+
, goldenUPlcReadable "integerABC" $$(compile [||integerABC||])
44+
, goldenUPlcReadable "head" $$(compile [||head||])
45+
]

plutus-tx-plugin/test/Spec.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import AsData.Budget.Spec qualified as AsData.Budget
55
import AssocMap.Spec qualified as AssocMap
66
import Blueprint.Tests qualified
77
import Budget.Spec qualified as Budget
8+
import BuiltinCasing.Spec qualified as BuiltinCasing
89
import BuiltinList.Budget.Spec qualified as BuiltinList.Budget
910
import BuiltinList.NoCasing.Spec qualified as BuiltinList.NoCasing
1011
import ByteStringLiterals.Spec qualified as ByteStringLiterals
@@ -64,4 +65,5 @@ tests =
6465
, embed List.propertyTests
6566
, Array.smokeTests
6667
, CallTrace.tests
68+
, BuiltinCasing.tests
6769
]

0 commit comments

Comments
 (0)