Skip to content

Commit b09cd50

Browse files
authored
Merge pull request #40 from input-output-hk/nc/huddleM
Introduce HuddleM
2 parents fcb9ed7 + 8c66e35 commit b09cd50

File tree

8 files changed

+253
-37
lines changed

8 files changed

+253
-37
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,10 @@
2222
## 0.3.2.0 -- 2024-09-11
2323

2424
* Leading rather than trailing commas in the pretty printer.
25+
26+
## 0.3.3.0 -- 2024-11-13
27+
28+
* Introduce HuddleM, another way to define a Huddle spec. This allows total
29+
control over the order that items are presented in the CDDL, at the cost
30+
of making it somewhat harder to re-use items (they need to be returned from
31+
the monad).

cuddle.cabal

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 3.4
22
name: cuddle
3-
version: 0.3.2.1
3+
version: 0.3.3.0
44
synopsis: CDDL Generator and test utilities
55

66
-- description:
@@ -50,6 +50,7 @@ library
5050
Codec.CBOR.Cuddle.CDDL.Postlude
5151
Codec.CBOR.Cuddle.CDDL.Resolve
5252
Codec.CBOR.Cuddle.Huddle
53+
Codec.CBOR.Cuddle.Huddle.HuddleM
5354
Codec.CBOR.Cuddle.Parser
5455
Codec.CBOR.Cuddle.Pretty
5556

@@ -70,6 +71,7 @@ library
7071
, mtl
7172
, mutable-containers
7273
, optics-core
74+
, ordered-containers
7375
, parser-combinators
7476
, prettyprinter
7577
, random
@@ -81,7 +83,9 @@ library
8183
executable example
8284
import: warnings, ghc2021
8385
default-language: Haskell2010
84-
other-modules: Conway
86+
other-modules:
87+
Conway
88+
Monad
8589

8690
-- other-extensions:
8791
hs-source-dirs: example

example/Main.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import Prettyprinter.Util (putDocW)
2222
import System.Environment (getArgs)
2323
import System.Random (getStdGen)
2424
import Text.Megaparsec (ParseErrorBundle, Parsec, errorBundlePretty, runParser)
25+
import qualified Monad
2526

2627
main :: IO ()
2728
main = do
@@ -65,6 +66,10 @@ main = do
6566
[] -> do
6667
let cw = toCDDL conway
6768
putDocW 80 $ pretty cw
69+
putStrLn "--------------------------------------"
70+
putDocW 80 $ pretty (toCDDL Monad.spec)
71+
putStrLn "--------------------------------------"
72+
putDocW 80 $ pretty (toCDDL Monad.spec2)
6873
_ -> putStrLn "Expected filename"
6974

7075
parseFromFile ::

example/Monad.hs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{-# LANGUAGE OverloadedLists #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE RecursiveDo #-}
4+
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
5+
{-# HLINT ignore "Use camelCase" #-}
6+
7+
module Monad where
8+
9+
import Codec.CBOR.Cuddle.Huddle qualified as Huddle
10+
import Codec.CBOR.Cuddle.Huddle.HuddleM
11+
import Data.Word (Word64)
12+
13+
hdl_set :: (IsType0 t0) => t0 -> GRuleCall
14+
hdl_set = Huddle.binding $ \x -> "set" Huddle.=:= arr [0 <+ a x]
15+
16+
spec :: Huddle
17+
spec = huddleDef $ mdo
18+
transaction <-
19+
"transaction"
20+
=:= mp
21+
[ idx 0 ==> set txIn,
22+
idx 1 ==> set txOut
23+
]
24+
txIn <- "txIn" =:= arr ["transaction_id" ==> hash32, "index" ==> txId]
25+
txOut <- "txOut" =:= arr [idx 0 ==> address, idx 1 ==> value]
26+
txId <- "txId" =:= VUInt `sized` (2 :: Word64)
27+
address <- "address" =:= VBytes `sized` (32 :: Word64)
28+
hash32 <- "hash32" =:= VBytes `sized` (32 :: Word64)
29+
value <- "value" =:= VUInt
30+
set <- include hdl_set
31+
32+
setRootRules [transaction]
33+
34+
spec2 :: Huddle
35+
spec2 =
36+
spec
37+
<> huddleDef
38+
( mdo
39+
set <- include hdl_set
40+
txIn <- unsafeIncludeFromHuddle spec "txIn"
41+
txOut <- unsafeIncludeFromHuddle spec "txOut"
42+
_transaction <-
43+
"transaction"
44+
=:= mp
45+
[ idx 0 ==> set txIn,
46+
idx 1 ==> set txOut,
47+
idx 2 ==> metadata
48+
]
49+
metadata <- "metadata" =:= VBytes
50+
_value <- "value" =:= mp ["token" ==> VText, "quantity" ==> VUInt]
51+
pure ()
52+
)

example/cddl-files/basic_assign.cddl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ header_body = [
1313
]
1414

1515
$kes_signature = bytes .size 32
16-
unit_interval<denominator> = [0..denominator, denominator]
16+
unit_interval<denominator> = [0 .. denominator, denominator]
1717

1818
unit_int = unit_interval<uint>
1919

@@ -28,3 +28,8 @@ usz4 = uint .size 4
2828
usz8 = uint .size 8
2929

3030
group = (usz4, usz8 / mysize, header_body, { * uint => coin })
31+
32+
set<a> = [ * a]
33+
set2<a> = set<a>
34+
35+
coin_bag = set2<coin>

src/Codec/CBOR/Cuddle/Huddle.hs

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,16 @@ import Data.ByteString (ByteString)
9494
import Data.Default.Class (Default (..))
9595
import Data.Generics.Product (field, getField)
9696
import Data.List.NonEmpty qualified as NE
97-
import Data.Map.Strict qualified as HaskMap
97+
import Data.Map.Ordered.Strict (OMap)
98+
import Data.Map.Ordered.Strict qualified as OMap
9899
import Data.String (IsString (fromString))
99100
import Data.Text qualified as T
100101
import Data.Tuple.Optics (Field2 (..))
101102
import Data.Void (Void)
102103
import Data.Word (Word64)
103104
import GHC.Exts (IsList (Item, fromList, toList))
104105
import GHC.Generics (Generic)
105-
import Optics.Core (view, (%~), (&), (.~))
106+
import Optics.Core (view, (%~), (&), (.~), (^.))
106107
import Prelude hiding ((/))
107108

108109
data Named a = Named
@@ -131,12 +132,40 @@ data HuddleItem
131132
data Huddle = Huddle
132133
{ -- | Root elements
133134
roots :: [Rule],
134-
items :: [HuddleItem]
135+
items :: OMap T.Text HuddleItem
135136
}
136137
deriving (Generic, Show)
137138

139+
-- | This semigroup instance:
140+
-- - Takes takes the roots from the RHS unless they are empty, in which case
141+
-- it takes the roots from the LHS
142+
-- - Uses the RHS to override items on the LHS where they share a name.
143+
-- The value from the RHS is taken, but the index from the LHS is used.
144+
--
145+
-- Note that this allows replacing items in the middle of a tree without
146+
-- updating higher-level items which make use of them - that is, we do not
147+
-- need to "close over" higher-level terms, since by the time they have been
148+
-- built into a huddle structure, the references have been converted to keys.
149+
instance Semigroup Huddle where
150+
h1 <> h2 =
151+
Huddle
152+
{ roots = case roots h2 of
153+
[] -> roots h1
154+
xs -> xs,
155+
items = OMap.unionWithL (\_ _ v2 -> v2) (items h1) (items h2)
156+
}
157+
158+
-- | This instance is mostly used for testing
159+
instance IsList Huddle where
160+
type Item Huddle = Rule
161+
fromList [] = Huddle mempty OMap.empty
162+
fromList (x : xs) =
163+
(field @"items" %~ (OMap.|> (x ^. field @"name", HIRule x))) $ fromList xs
164+
165+
toList = const []
166+
138167
instance Default Huddle where
139-
def = Huddle [] []
168+
def = Huddle [] OMap.empty
140169

141170
data Choice a
142171
= NoChoice a
@@ -505,6 +534,13 @@ instance IsType0 GRef where
505534
instance (IsType0 a) => IsType0 (Tagged a) where
506535
toType0 = NoChoice . T2Tagged . fmap toType0
507536

537+
instance IsType0 HuddleItem where
538+
toType0 (HIRule r) = toType0 r
539+
toType0 (HIGroup g) = toType0 g
540+
toType0 (HIGRule g) =
541+
error $
542+
"Attempt to reference generic rule from HuddleItem not supported: " <> show g
543+
508544
class CanQuantify a where
509545
-- | Apply a lower bound
510546
(<+) :: Word64 -> a -> a
@@ -829,17 +865,17 @@ collectFrom topRs =
829865
toHuddle $
830866
execState
831867
(traverse goRule topRs)
832-
HaskMap.empty
868+
OMap.empty
833869
where
834870
toHuddle items =
835871
Huddle
836872
{ roots = topRs,
837-
items = view _2 <$> HaskMap.toList items
873+
items = items
838874
}
839875
goRule r@(Named n t0 _) = do
840876
items <- get
841-
when (HaskMap.notMember n items) $ do
842-
modify (HaskMap.insert n (HIRule r))
877+
when (OMap.notMember n items) $ do
878+
modify (OMap.|> (n, HIRule r))
843879
goT0 t0
844880
goChoice f (NoChoice x) = f x
845881
goChoice f (ChoiceOf x xs) = f x >> goChoice f xs
@@ -850,13 +886,13 @@ collectFrom topRs =
850886
goT2 (T2Ref n) = goRule n
851887
goT2 (T2Group r@(Named n g _)) = do
852888
items <- get
853-
when (HaskMap.notMember n items) $ do
854-
modify (HaskMap.insert n (HIGroup r))
889+
when (OMap.notMember n items) $ do
890+
modify (OMap.|> (n, HIGroup r))
855891
goGroup g
856892
goT2 (T2Generic r@(Named n g _)) = do
857893
items <- get
858-
when (HaskMap.notMember n items) $ do
859-
modify (HaskMap.insert n (HIGRule $ fmap callToDef r))
894+
when (OMap.notMember n items) $ do
895+
modify (OMap.|> (n, HIGRule $ fmap callToDef r))
860896
goT0 (body g)
861897
-- Note that the parameters here may be different, so this doesn't live
862898
-- under the guard
@@ -890,7 +926,7 @@ toCDDL' mkPseudoRoot hdl =
890926
then (toTopLevelPseudoRoot (roots hdl) NE.<|)
891927
else id
892928
)
893-
$ fmap toCDDLItem (NE.fromList $ items hdl)
929+
$ fmap toCDDLItem (NE.fromList $ fmap (view _2) $ toList $ items hdl)
894930
where
895931
toCDDLItem (HIRule r) = toCDDLRule r
896932
toCDDLItem (HIGroup g) = toCDDLGroup g
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
-- | Monad for declaring Huddle constructs
2+
module Codec.CBOR.Cuddle.Huddle.HuddleM
3+
( module Huddle,
4+
(=:=),
5+
(=:~),
6+
(=::=),
7+
binding,
8+
setRootRules,
9+
huddleDef,
10+
huddleDef',
11+
include,
12+
unsafeIncludeFromHuddle,
13+
)
14+
where
15+
16+
import Codec.CBOR.Cuddle.Huddle hiding (binding, (=:=), (=:~))
17+
import Codec.CBOR.Cuddle.Huddle qualified as Huddle
18+
import Control.Monad.State.Strict (State, modify, runState)
19+
import Data.Default.Class (def)
20+
import Data.Generics.Product (HasField (..))
21+
import Data.Map.Ordered.Strict qualified as OMap
22+
import Data.Text qualified as T
23+
import Optics.Core (set, (%~), (^.))
24+
25+
type HuddleM = State Huddle
26+
27+
-- | Overridden version of assignment which also adds the rule to the state
28+
(=:=) :: (IsType0 a) => T.Text -> a -> HuddleM Rule
29+
n =:= b = let r = n Huddle.=:= b in include r
30+
31+
infixl 1 =:=
32+
33+
-- | Overridden version of group assignment which adds the rule to the state
34+
(=:~) :: T.Text -> Group -> HuddleM (Named Group)
35+
n =:~ b = let r = n Huddle.=:~ b in include r
36+
37+
infixl 1 =:~
38+
39+
binding ::
40+
forall t0.
41+
(IsType0 t0) =>
42+
(GRef -> Rule) ->
43+
HuddleM (t0 -> GRuleCall)
44+
binding fRule = include (Huddle.binding fRule)
45+
46+
-- | Renamed version of Huddle's underlying '=:=' for use in generic bindings
47+
(=::=) :: (IsType0 a) => T.Text -> a -> Rule
48+
n =::= b = n Huddle.=:= b
49+
50+
infixl 1 =::=
51+
52+
setRootRules :: [Rule] -> HuddleM ()
53+
setRootRules = modify . set (field @"roots")
54+
55+
huddleDef :: HuddleM a -> Huddle
56+
huddleDef = snd . huddleDef'
57+
58+
huddleDef' :: HuddleM a -> (a, Huddle)
59+
huddleDef' mh = runState mh def
60+
61+
class Includable a where
62+
-- | Include a rule, group, or generic rule defined elsewhere
63+
include :: a -> HuddleM a
64+
65+
instance Includable Rule where
66+
include r =
67+
modify (field @"items" %~ (OMap.|> (r ^. field @"name", HIRule r)))
68+
>> pure r
69+
70+
instance Includable (Named Group) where
71+
include r =
72+
modify
73+
( (field @"items")
74+
%~ (OMap.|> (r ^. field @"name", HIGroup r))
75+
)
76+
>> pure r
77+
78+
instance (IsType0 t0) => Includable (t0 -> GRuleCall) where
79+
include gr =
80+
let fakeT0 = error "Attempting to unwrap fake value in generic call"
81+
grDef = callToDef <$> gr fakeT0
82+
n = grDef ^. field @"name"
83+
in do
84+
modify (field @"items" %~ (OMap.|> (n, HIGRule grDef)))
85+
pure gr
86+
87+
instance Includable HuddleItem where
88+
include x@(HIRule r) = include r >> pure x
89+
include x@(HIGroup g) = include g >> pure x
90+
include x@(HIGRule g) =
91+
let n = g ^. field @"name"
92+
in do
93+
modify (field @"items" %~ (OMap.|> (n, x)))
94+
pure x
95+
96+
unsafeIncludeFromHuddle ::
97+
Huddle ->
98+
T.Text ->
99+
HuddleM HuddleItem
100+
unsafeIncludeFromHuddle h name =
101+
let items = h ^. field @"items"
102+
in case OMap.lookup name items of
103+
Just v -> include v
104+
Nothing -> error $ show name <> " was not found in Huddle spec"

0 commit comments

Comments
 (0)