lorentz-0.4.0: src/Lorentz/UStore/Haskell.hs
-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
-- | Conversion between 'UStore' in Haskell and Michelson representation.
module Lorentz.UStore.Haskell
( mkUStore
, MkUStoreTW
, ustoreDecompose
, ustoreDecomposeFull
, DecomposeUStoreTW
, fillUStore
, migrateFillUStore
, fillUStoreMigrationBlock
, FillUStoreTW
) where
import qualified Unsafe
import Control.Monad.Except (runExcept, throwError)
import qualified Data.List as L
import qualified Data.Map as Map
import Data.Singletons (demote)
import Fcf (type (=<<), Eval, Pure2)
import qualified Fcf
import Fmt ((+|), (+||), (|+), (||+))
import Lorentz.Base
import Lorentz.Coercions
import Lorentz.Constraints
import qualified Lorentz.Instr as L
import Lorentz.Pack
import Lorentz.UStore.Migration
import Lorentz.UStore.Migration.Diff
import Lorentz.UStore.Traversal
import Lorentz.UStore.Types
import Michelson.Text
import Michelson.Typed.Haskell.Value
import Util.Type
-- | 'UStore' content represented as key-value pairs.
type UStoreContent = [(ByteString, ByteString)]
-- | Make 'UStore' from separate @big_map@s and fields.
mkUStore
:: (UStoreTraversable MkUStoreTW template)
=> template -> UStore template
mkUStore = UStore . BigMap . mkUStoreInternal
-- | Decompose 'UStore' into separate @big_map@s and fields.
--
-- Since this function needs to @UNPACK@ content of @UStore@ to actual
-- keys and values, you have to provide 'UnpackEnv'.
--
-- Along with resulting value, you get a list of @UStore@ entries which
-- were not recognized as belonging to any submap or field according to
-- @UStore@'s template - this should be empty unless @UStore@ invariants
-- were violated.
ustoreDecompose
:: forall template.
(UStoreTraversable DecomposeUStoreTW template)
=> UStore template -> Either Text (UStoreContent, template)
ustoreDecompose = storeDecomposeInternal . Map.toList . unBigMap . unUStore
-- | Like 'ustoreDecompose', but requires all entries from @UStore@ to be
-- recognized.
ustoreDecomposeFull
:: forall template.
(UStoreTraversable DecomposeUStoreTW template)
=> UStore template -> Either Text template
ustoreDecomposeFull ustore = do
(remained, res) <- ustoreDecompose ustore
unless (null remained) $
Left $ "Unrecognized entries in UStore: " +|| remained ||+ ""
return res
-- | Make migration script which initializes 'UStore' from scratch.
fillUStore
:: (UStoreTraversable FillUStoreTW template)
=> template -> UStoreMigration () template
fillUStore v = UStoreMigration $ fillUStoreInternal v
-- | Version of 'migrateFillUStore' for batched migrations.
--
-- Each field write will be placed to a separate batch.
fillUStoreMigrationBlock
:: ( UStoreTraversable FillUStoreTW template
, allFieldsExp ~ AllUStoreFieldsF template
, newDiff ~ FillingNewDiff template diff
, newTouched ~ FillingNewTouched template touched
, PatternMatchL newDiff, PatternMatchL newTouched
)
=> template
-> MigrationBlocks oldTempl newTempl diff touched newDiff newTouched
fillUStoreMigrationBlock v = MigrationBlocks $ fillUStoreInternal v
-- | Fill 'UStore' with entries from the given template as part of simple
-- migration.
--
-- Sometimes you already have some fields initialized and 'fillUStore' does not
-- suit, then in case if your UStore template is a nested structure you can use
-- sub-templates to initialize the corresponding parts of UStore.
--
-- For batched migrations see 'fillUStoreMigrationBlock'.
migrateFillUStore
:: ( UStoreTraversable FillUStoreTW template
, allFieldsExp ~ AllUStoreFieldsF template
, newDiff ~ FillingNewDiff template diff
, newTouched ~ FillingNewTouched template touched
, PatternMatchL newDiff, PatternMatchL newTouched
)
=> template
-> Lambda
(MUStore oldTempl newTempl diff touched)
(MUStore oldTempl newTempl newDiff newTouched)
migrateFillUStore v =
let atoms = fillUStoreInternal v
script = foldMap (unMigrationScript . maScript) atoms
in forcedCoerce_ # script # forcedCoerce_
type FillingNewDiff template diff =
CoverDiffMany diff
(Eval (Fcf.Map (Pure2 '(,) 'DcAdd) =<< LinearizeUStoreF template))
type FillingNewTouched template touched =
Eval (AllUStoreFieldsF template) ++ touched
-- Implementation
----------------------------------------------------------------------------
-- | Internal helper for 'mkUStore'.
mkUStoreInternal
:: (UStoreTraversable MkUStoreTW template)
=> template -> Map ByteString ByteString
mkUStoreInternal = foldUStore MkUStoreTW
-- | Internal helper for 'ustoreDecompose'.
storeDecomposeInternal
:: forall template.
(UStoreTraversable DecomposeUStoreTW template)
=> UStoreContent -> Either Text (UStoreContent, template)
storeDecomposeInternal =
runExcept . fmap swap . runStateT (genUStore DecomposeUStoreTW)
-- | Internal helper for 'fillUStore'.
fillUStoreInternal
:: (UStoreTraversable FillUStoreTW template)
=> template
-> [MigrationAtom]
fillUStoreInternal a = appEndo (foldUStore FillUStoreTW a) []
-- | Declares handlers for UStore creation from template.
data MkUStoreTW = MkUStoreTW
instance UStoreTraversalWay MkUStoreTW where
type UStoreTraversalArgumentWrapper MkUStoreTW = Identity
type UStoreTraversalMonad MkUStoreTW = Const (Map ByteString ByteString)
instance (NicePackedValue val) =>
UStoreTraversalFieldHandler MkUStoreTW marker val where
ustoreTraversalFieldHandler MkUStoreTW fieldName (Identity val) =
Const $
one ( mkFieldMarkerUKeyL @marker fieldName
, lPackValue val
)
instance (NicePackedValue k, NicePackedValue v) =>
UStoreTraversalSubmapHandler MkUStoreTW k v where
ustoreTraversalSubmapHandler MkUStoreTW fieldName (Identity m) =
Const $
mconcat
[ one ( lPackValue (labelToMText fieldName, k)
, lPackValue v
)
| (k, v) <- Map.toList m
]
-- | Declares handlers for UStore conversion to template.
data DecomposeUStoreTW = DecomposeUStoreTW
instance UStoreTraversalWay DecomposeUStoreTW where
type UStoreTraversalArgumentWrapper DecomposeUStoreTW = Const ()
type UStoreTraversalMonad DecomposeUStoreTW =
StateT UStoreContent (ExceptT Text Identity)
instance (NiceUnpackedValue val) =>
UStoreTraversalFieldHandler DecomposeUStoreTW marker val where
ustoreTraversalFieldHandler DecomposeUStoreTW fieldName (Const ()) = do
let expectedKey = mkFieldMarkerUKey @marker (labelToMText fieldName)
allMatched <- mapMaybesState $ \(key, value) -> do
unless (key == expectedKey) mzero
case lUnpackValue value of
Left err -> throwError $
"Failed to parse UStore value for field " +|
demote @(ToT val) |+ ": " +| err |+ ""
Right valValue ->
pure valValue
case allMatched of
[] -> throwError $
"Failed to find field in UStore: " +| labelToMText fieldName |+ ""
[matched] ->
pure matched
(_ : _ : _) ->
error "UStore content contained multiple entries with the same key"
instance (Ord k, NiceUnpackedValue k, NiceUnpackedValue v) =>
UStoreTraversalSubmapHandler DecomposeUStoreTW k v where
ustoreTraversalSubmapHandler _ fieldName (Const ()) =
fmap Map.fromList $
mapMaybesState $ \(key, value) ->
case lUnpackValue @(UStoreSubmapKey k) key of
Left _ -> mzero
Right (name :: MText, keyValue :: k)
| name /= labelToMText fieldName ->
mzero
| otherwise ->
case lUnpackValue value of
Left err -> throwError $
"Failed to parse UStore value for " +|
demote @(ToT k) |+ " |~> " +| demote @(ToT v) |+
": " +| err |+ ""
Right valValue ->
pure (keyValue, valValue)
-- | Declares handlers for UStore filling via lambda.
data FillUStoreTW = FillUStoreTW
instance UStoreTraversalWay FillUStoreTW where
type UStoreTraversalArgumentWrapper FillUStoreTW = Identity
type UStoreTraversalMonad FillUStoreTW = Const (Endo [MigrationAtom])
instance (NiceConstant v) =>
UStoreTraversalFieldHandler FillUStoreTW marker v where
ustoreTraversalFieldHandler FillUStoreTW fieldName (Identity val) =
Const $
Endo . (:) . formMigrationAtom Nothing $
attachMigrationActionName (DAddAction "init field") fieldName (Proxy @v) #
-- Not pushing already packed value (which would be more efficient) because
-- analyzers cannot work with packed values.
-- TODO: make optimizer compress this to @push (Just $ lPackValue val)@
L.push val # L.pack # L.some #
L.push (mkFieldMarkerUKeyL @marker fieldName) #
L.update
instance (NiceConstant k, NiceConstant v) =>
UStoreTraversalSubmapHandler FillUStoreTW k v where
ustoreTraversalSubmapHandler _ fieldName (Identity m) =
Const $
Endo . (<>) $
Map.toList m <&> \(k, v) ->
formMigrationAtom Nothing $
attachMigrationActionName (DAddAction "init submap") fieldName (Proxy @v) #
-- @PUSH + PACK@ will be merged by optimizer, but there is still place
-- for further improvement both or value and key pushing.
-- We cannot push already packed value because that would break code
-- analyzers and transformers, consider adding necessary rules to
-- optimizer.
-- TODO [TM-379]: consider improving this case
-- or
-- TODO: add necessary rules to optimizer
L.push v # L.pack # L.some #
L.push k # L.push (labelToMText fieldName) # L.pair #
L.pack @(UStoreSubmapKey _) #
L.update
-- | Tries to map all items in the state and returns those which were mapped
-- successfully; others are retained in the state.
mapMaybesState :: forall a b m. MonadState [a] m => (a -> MaybeT m b) -> m [b]
mapMaybesState mapper =
get >>= \st -> do
mapped <- mapM (\a -> (a, ) <$> runMaybeT (mapper a)) st
let
(passed, failed) =
bimap (map (Unsafe.fromJust . snd)) (map fst) $
L.partition @(a, Maybe b) (isJust . snd) $
mapped
put failed
return passed
-- Examples
----------------------------------------------------------------------------
data MyStoreTemplate = MyStoreTemplate
{ ints :: Integer |~> ()
, flag :: UStoreField Bool
}
deriving stock (Generic)
data MyStoreTemplateBig = MyStoreTemplateBig
{ templ :: MyStoreTemplate
, bytes :: ByteString |~> ByteString
}
deriving stock (Generic)
_storeSample :: UStore MyStoreTemplate
_storeSample = mkUStore
MyStoreTemplate
{ ints = UStoreSubMap $ one (1, ())
, flag = UStoreField False
}
_storeSampleBig :: UStore MyStoreTemplateBig
_storeSampleBig = mkUStore $
MyStoreTemplateBig
MyStoreTemplate
{ ints = UStoreSubMap $ one (1, ())
, flag = UStoreField False
}
(UStoreSubMap $ one ("a", "b"))