Skip to content

Commit 46164ce

Browse files
zliu41Unisay
authored andcommitted
Add insertCoin and unionValue implementations and tests (#7322)
1 parent 68be3c0 commit 46164ce

File tree

10 files changed

+320
-60
lines changed

10 files changed

+320
-60
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
### Added
3+
4+
- Built-in type `Value`, and the implementations of `insertCoin` and `unionValue`.

plutus-core/plutus-core.cabal

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ test-suite plutus-core-test
378378
Parser.Spec
379379
Pretty.Readable
380380
TypeSynthesis.Spec
381+
Value.Spec
381382

382383
default-language: Haskell2010
383384
build-depends:
@@ -396,6 +397,8 @@ test-suite plutus-core-test
396397
, plutus-core ^>=1.53
397398
, plutus-core:plutus-core-testlib
398399
, prettyprinter
400+
, QuickCheck
401+
, safe
399402
, serialise
400403
, tasty
401404
, tasty-golden
@@ -798,6 +801,7 @@ library plutus-core-testlib
798801
PlutusCore.Generators.QuickCheck.Substitutions
799802
PlutusCore.Generators.QuickCheck.Unification
800803
PlutusCore.Generators.QuickCheck.Utils
804+
PlutusCore.Generators.QuickCheck.Value
801805
PlutusCore.Test
802806
PlutusIR.Generators.AST
803807
PlutusIR.Generators.QuickCheck
@@ -813,6 +817,7 @@ library plutus-core-testlib
813817

814818
build-depends:
815819
, base >=4.9 && <5
820+
, base16-bytestring
816821
, bifunctors
817822
, bytestring
818823
, containers
@@ -836,6 +841,7 @@ library plutus-core-testlib
836841
, QuickCheck
837842
, quickcheck-instances
838843
, quickcheck-transformer
844+
, random
839845
, size-based
840846
, Stream
841847
, tagged

plutus-core/plutus-core/src/PlutusCore/Value.hs

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE DeriveAnyClass #-}
22
{-# LANGUAGE FlexibleInstances #-}
3+
{-# LANGUAGE LambdaCase #-}
34
{-# LANGUAGE ViewPatterns #-}
45

56
module PlutusCore.Value (
@@ -10,8 +11,12 @@ module PlutusCore.Value (
1011
empty,
1112
fromList,
1213
toList,
14+
toFlatList,
1315
totalSize,
1416
maxInnerSize,
17+
insertCoin,
18+
deleteCoin,
19+
unionValue,
1520
) where
1621

1722
import Codec.Serialise (Serialise)
@@ -22,8 +27,10 @@ import Data.ByteString.Base64 qualified as Base64
2227
import Data.Hashable (Hashable)
2328
import Data.IntMap.Strict (IntMap)
2429
import Data.IntMap.Strict qualified as IntMap
30+
import Data.Map.Merge.Strict qualified as M
2531
import Data.Map.Strict (Map)
2632
import Data.Map.Strict qualified as Map
33+
import Data.Maybe
2734
import Data.Text.Encoding qualified as Text
2835
import GHC.Generics
2936

@@ -56,32 +63,50 @@ The map is guaranteed to not contain empty inner map or zero amount.
5663
-}
5764
unpack :: Value -> NestedMap
5865
unpack (Value v _ _) = v
66+
{-# INLINE unpack #-}
5967

6068
{-| Pack a map from (currency symbol, token name) to amount into a `Value`.
6169
6270
The map will be filtered so that it does not contain empty inner map or zero amount.
6371
-}
6472
pack :: NestedMap -> Value
65-
pack (normalize -> v) = Value v sizes size
73+
pack = pack' . normalize
74+
{-# INLINE pack #-}
75+
76+
-- | Like `pack` but does not normalize.
77+
pack' :: NestedMap -> Value
78+
pack' (normalize -> v) = Value v sizes size
6679
where
67-
sizes = Map.foldr' (IntMap.alter (maybe (Just 1) (Just . (+ 1))) . Map.size) mempty v
68-
size = Map.foldr' ((+) . Map.size) 0 v
80+
(sizes, size) = Map.foldl' alg (mempty, 0) v
81+
alg (ss, s) inner =
82+
( IntMap.alter (maybe (Just 1) (Just . (+ 1))) (Map.size inner) ss
83+
, s + Map.size inner
84+
)
85+
{-# INLINEABLE pack' #-}
6986

7087
{-| Total size, i.e., the number of distinct `(currency symbol, token name)` pairs
7188
contained in the `Value`.
7289
-}
7390
totalSize :: Value -> Int
7491
totalSize (Value _ _ size) = size
92+
{-# INLINE totalSize #-}
7593

7694
-- | Size of the largest inner map.
7795
maxInnerSize :: Value -> Int
7896
maxInnerSize (Value _ sizes _) = maybe 0 fst (IntMap.lookupMax sizes)
97+
{-# INLINE maxInnerSize #-}
7998

8099
empty :: Value
81100
empty = Value mempty mempty 0
101+
{-# INLINE empty #-}
82102

83103
toList :: Value -> [(ByteString, [(ByteString, Integer)])]
84104
toList = Map.toList . Map.map Map.toList . unpack
105+
{-# INLINEABLE toList #-}
106+
107+
toFlatList :: Value -> [(ByteString, ByteString, Integer)]
108+
toFlatList (toList -> xs) = [(c, t, a) | (c, ys) <- xs, (t, a) <- ys]
109+
{-# INLINEABLE toFlatList #-}
85110

86111
fromList :: [(ByteString, [(ByteString, Integer)])] -> Value
87112
fromList =
@@ -91,8 +116,85 @@ fromList =
91116

92117
normalize :: NestedMap -> NestedMap
93118
normalize = Map.filter (not . Map.null) . Map.map (Map.filter (/= 0))
119+
{-# INLINEABLE normalize #-}
94120

95121
instance Pretty Value where
96122
pretty = pretty . fmap (bimap toText (fmap (first toText))) . toList
97123
where
98124
toText = Text.decodeLatin1 . Base64.encode
125+
126+
{-| \(O(\log \max(m, k))\), where \(m\) is the size of the outer map, and \(k\) is
127+
the size of the largest inner map.
128+
-}
129+
insertCoin :: ByteString -> ByteString -> Integer -> Value -> Value
130+
insertCoin currency token amt v@(Value outer sizes size)
131+
| amt == 0 = deleteCoin currency token v
132+
| otherwise =
133+
let (mold, outer') = Map.alterF f currency outer
134+
(sizes', size') = case mold of
135+
Just old -> (updateSizes old (old + 1) sizes, size + 1)
136+
Nothing -> (sizes, size)
137+
in Value outer' sizes' size'
138+
where
139+
f
140+
:: Maybe (Map ByteString Integer)
141+
-> ( Maybe Int -- Just (old size of inner map) if the total size grows by 1, otherwise Nothing
142+
, Maybe (Map ByteString Integer)
143+
)
144+
f = \case
145+
Nothing -> (Just 0, Just (Map.singleton token amt))
146+
Just inner ->
147+
let (isJust -> exists, inner') = Map.insertLookupWithKey (\_ _ _ -> amt) token amt inner
148+
in (if exists then Nothing else Just (Map.size inner), Just inner')
149+
{-# INLINEABLE insertCoin #-}
150+
151+
-- TODO: implement properly
152+
deleteCoin :: ByteString -> ByteString -> Value -> Value
153+
deleteCoin currency token (Value outer _ _) =
154+
pack $ case Map.lookup currency outer of
155+
Nothing -> outer
156+
Just inner -> Map.insert currency (Map.delete token inner) outer
157+
158+
{-| The precise complexity is complicated, but an upper bound
159+
is \(O(n_{1} \log n_{2}) + O(m)\), where \(n_{1}\) is the total size of the smaller
160+
value, \(n_{2}\) is the total size of the bigger value, and \(m\) is the
161+
combined size of the outer maps.
162+
-}
163+
unionValue :: Value -> Value -> Value
164+
unionValue (unpack -> vA) (unpack -> vB) =
165+
pack' $
166+
M.merge
167+
M.preserveMissing
168+
M.preserveMissing
169+
( M.zipWithMaybeMatched $ \_ innerA innerB ->
170+
let inner =
171+
M.merge
172+
M.preserveMissing
173+
M.preserveMissing
174+
( M.zipWithMaybeMatched $ \_ x y ->
175+
let z = x + y in if z == 0 then Nothing else Just z
176+
)
177+
innerA
178+
innerB
179+
in if Map.null inner
180+
then Nothing
181+
else
182+
Just inner
183+
)
184+
vA
185+
vB
186+
{-# INLINEABLE unionValue #-}
187+
188+
-- | Decrement bucket @old@, and increment bucket @new@.
189+
updateSizes :: Int -> Int -> IntMap Int -> IntMap Int
190+
updateSizes old new = dec . inc
191+
where
192+
inc =
193+
if new == 0
194+
then id
195+
else IntMap.alter (maybe (Just 1) (Just . (+ 1))) new
196+
dec =
197+
if old == 0
198+
then id
199+
else IntMap.update (\n -> if n <= 1 then Nothing else Just (n - 1)) old
200+
{-# INLINEABLE updateSizes #-}

plutus-core/plutus-core/test/Spec.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import Normalization.Type
2323
import Parser.Spec qualified as Parser
2424
import Pretty.Readable
2525
import TypeSynthesis.Spec (test_typecheck)
26+
import Value.Spec qualified as Value
2627

2728
import PlutusCore
2829
import PlutusCore.Check.Uniques qualified as Uniques
@@ -257,5 +258,6 @@ allTests plcFiles rwFiles typeFiles typeErrorFiles =
257258
, Check.tests
258259
, NEAT.tests
259260
, Parser.tests
261+
, Value.tests
260262
, test_utils
261263
]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
{-# LANGUAGE ViewPatterns #-}
3+
{-# OPTIONS_GHC -fno-warn-orphans #-}
4+
5+
module Value.Spec (tests) where
6+
7+
import Data.Foldable qualified as F
8+
import Data.Map.Strict qualified as Map
9+
import Data.Maybe
10+
import PlutusCore.Generators.QuickCheck.Builtin ()
11+
import PlutusCore.Generators.QuickCheck.Value (genShortHex)
12+
import PlutusCore.Value (Value)
13+
import PlutusCore.Value qualified as V
14+
import Safe.Foldable (maximumMay)
15+
import Test.QuickCheck
16+
import Test.Tasty
17+
import Test.Tasty.QuickCheck
18+
19+
prop_packUnpackRoundtrip :: Property
20+
prop_packUnpackRoundtrip = forAll arbitrary $ \v ->
21+
v === V.pack (V.unpack v)
22+
23+
-- | Verifies that @pack@ correctly updates the sizes
24+
prop_packBookkeeping :: Property
25+
prop_packBookkeeping = forAll arbitrary $ \nm ->
26+
checkSizes (V.pack nm)
27+
28+
{-| Verifies that @pack@ preserves @Value@ invariants, i.e.,
29+
no empty inner map or zero amount.
30+
-}
31+
prop_packPreservesInvariants :: Property
32+
prop_packPreservesInvariants = forAll arbitrary $ \nm ->
33+
checkInvariants (V.pack nm)
34+
35+
-- | Verifies that @insertCoin@ correctly updates the sizes
36+
prop_insertCoinBookkeeping :: Property
37+
prop_insertCoinBookkeeping = forAll arbitrary $ \(v, amt) ->
38+
forAll (genShortHex (V.totalSize v)) $ \currency ->
39+
forAll (genShortHex (V.totalSize v)) $ \token ->
40+
let v' = V.insertCoin currency token amt v
41+
in checkSizes v'
42+
43+
-- | Verifies that @insertCoin@ preserves @Value@ invariants
44+
prop_insertCoinPreservesInvariants :: Property
45+
prop_insertCoinPreservesInvariants = forAll arbitrary $ \(v, amt) ->
46+
forAll (genShortHex (V.totalSize v)) $ \currency ->
47+
forAll (genShortHex (V.totalSize v)) $ \token ->
48+
let v' = V.insertCoin currency token amt v
49+
in checkInvariants v'
50+
51+
prop_unionCommutative :: Property
52+
prop_unionCommutative = forAll arbitrary $ \(v, v') ->
53+
V.unionValue v v' === V.unionValue v' v
54+
55+
prop_unionAssociative :: Property
56+
prop_unionAssociative = forAll arbitrary $ \(v1, v2, v3) ->
57+
V.unionValue v1 (V.unionValue v2 v3) === V.unionValue (V.unionValue v1 v2) v3
58+
59+
prop_insertCoinIdempotent :: Property
60+
prop_insertCoinIdempotent = forAll arbitrary $ \v ->
61+
let fm = V.toFlatList v
62+
in v === F.foldl' (\acc (c, t, a) -> V.insertCoin c t a acc) v fm
63+
64+
checkSizes :: Value -> Property
65+
checkSizes v =
66+
(expectedMaxInnerSize === actualMaxInnerSize)
67+
.&&. (expectedSize === actualSize)
68+
where
69+
expectedMaxInnerSize = fromMaybe 0 . maximumMay $ Map.map Map.size (V.unpack v)
70+
actualMaxInnerSize = V.maxInnerSize v
71+
expectedSize = sum $ Map.map Map.size (V.unpack v)
72+
actualSize = V.totalSize v
73+
74+
checkInvariants :: Value -> Property
75+
checkInvariants (V.unpack -> v) =
76+
property ((not . any Map.null) v)
77+
.&&. property ((not . any (elem 0)) v)
78+
79+
tests :: TestTree
80+
tests =
81+
testGroup
82+
"Value"
83+
[ testProperty
84+
"packUnpackRoundtrip"
85+
prop_packUnpackRoundtrip
86+
, testProperty
87+
"packBookkeeping"
88+
prop_packBookkeeping
89+
, testProperty
90+
"packPreservesInvariants"
91+
prop_packPreservesInvariants
92+
, testProperty
93+
"insertCoinBookkeeping"
94+
prop_insertCoinBookkeeping
95+
, testProperty
96+
"insertCoinPreservesInvariants"
97+
prop_insertCoinPreservesInvariants
98+
, testProperty
99+
"unionCommutative"
100+
prop_unionCommutative
101+
, testProperty
102+
"unionAssociative"
103+
prop_unionAssociative
104+
, testProperty
105+
"insertCoinIdempotent"
106+
prop_insertCoinIdempotent
107+
]

plutus-core/testlib/PlutusCore/Generators/QuickCheck/Builtin.hs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import PlutusCore.Crypto.BLS12_381.Pairing qualified as BLS12_381.Pairing
2121
import PlutusCore.Data
2222
import PlutusCore.Generators.QuickCheck.GenerateKinds ()
2323
import PlutusCore.Generators.QuickCheck.Split (multiSplit0, multiSplit1, multiSplit1In)
24+
import PlutusCore.Generators.QuickCheck.Value ()
2425
import PlutusCore.Value (Value)
25-
import PlutusCore.Value qualified as Value
2626

2727
import Data.ByteString (ByteString, empty)
2828
import Data.Int
@@ -245,10 +245,6 @@ instance Arbitrary Data where
245245
arbitrary = arbitraryBuiltin
246246
shrink = shrinkBuiltin
247247

248-
instance Arbitrary Value where
249-
arbitrary = Value.pack <$> arbitrary
250-
shrink = fmap Value.pack . shrink . Value.unpack
251-
252248
instance ArbitraryBuiltin Value
253249

254250
instance ArbitraryBuiltin BLS12_381.G1.Element where

0 commit comments

Comments
 (0)