Skip to content

Commit 3358bdd

Browse files
committed
Switch to using an ordered map for Huddle
By using an ordered map we allow a lot of information for sorting the entries arising from Huddle, and in particular for _merging_ such entries, where we want to override some definitions.
1 parent 6d182e7 commit 3358bdd

File tree

4 files changed

+77
-40
lines changed

4 files changed

+77
-40
lines changed

cuddle.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ library
7171
, mtl
7272
, mutable-containers
7373
, optics-core
74+
, ordered-containers
7475
, parser-combinators
7576
, prettyprinter
7677
, random

src/Codec/CBOR/Cuddle/Huddle.hs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,17 @@ 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
9897
import Data.String (IsString (fromString))
9998
import Data.Text qualified as T
10099
import Data.Tuple.Optics (Field2 (..))
101100
import Data.Void (Void)
102101
import Data.Word (Word64)
103102
import GHC.Exts (IsList (Item, fromList, toList))
104103
import GHC.Generics (Generic)
105-
import Optics.Core (view, (%~), (&), (.~))
104+
import Optics.Core (view, (%~), (&), (.~), (^.))
106105
import Prelude hiding ((/))
106+
import Data.Map.Ordered.Strict qualified as OMap
107+
import Data.Map.Ordered.Strict (OMap)
107108

108109
data Named a = Named
109110
{ name :: T.Text,
@@ -131,13 +132,36 @@ 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

138-
instance Default Huddle where
139-
def = Huddle [] []
139+
-- | This semigroup instance takes the roots from the RHS and uses the
140+
-- RHS to override items on the LHS where they share a name.
141+
-- The value from the RHS is taken, but the index from the LHS is used.
142+
--
143+
-- Note that this allows replacing items in the middle of a tree without
144+
-- updating higher-level items which make use of them - that is, we do not
145+
-- need to "close over" higher-level terms, since by the time they have been
146+
-- built into a huddle structure, the references have been converted to keys.
147+
instance Semigroup Huddle where
148+
h1 <> h2 = Huddle {
149+
roots = roots h2,
150+
items = OMap.unionWithL (\_ _ v2 -> v2) (items h1) (items h2)
151+
}
140152

153+
-- | This instance is mostly used for testing
154+
instance IsList Huddle where
155+
type Item Huddle = Rule
156+
fromList [] = Huddle mempty OMap.empty
157+
fromList (x : xs) =
158+
(field @"items" %~ (OMap.|> (x ^. field @"name", HIRule x))) $ fromList xs
159+
160+
toList = const []
161+
162+
instance Default Huddle where
163+
def = Huddle [] OMap.empty
164+
141165
data Choice a
142166
= NoChoice a
143167
| ChoiceOf a (Choice a)
@@ -829,17 +853,17 @@ collectFrom topRs =
829853
toHuddle $
830854
execState
831855
(traverse goRule topRs)
832-
HaskMap.empty
856+
OMap.empty
833857
where
834858
toHuddle items =
835859
Huddle
836860
{ roots = topRs,
837-
items = view _2 <$> HaskMap.toList items
861+
items = items
838862
}
839863
goRule r@(Named n t0 _) = do
840864
items <- get
841-
when (HaskMap.notMember n items) $ do
842-
modify (HaskMap.insert n (HIRule r))
865+
when (OMap.notMember n items) $ do
866+
modify (OMap.|> (n, HIRule r))
843867
goT0 t0
844868
goChoice f (NoChoice x) = f x
845869
goChoice f (ChoiceOf x xs) = f x >> goChoice f xs
@@ -850,13 +874,13 @@ collectFrom topRs =
850874
goT2 (T2Ref n) = goRule n
851875
goT2 (T2Group r@(Named n g _)) = do
852876
items <- get
853-
when (HaskMap.notMember n items) $ do
854-
modify (HaskMap.insert n (HIGroup r))
877+
when (OMap.notMember n items) $ do
878+
modify (OMap.|> (n, HIGroup r))
855879
goGroup g
856880
goT2 (T2Generic r@(Named n g _)) = do
857881
items <- get
858-
when (HaskMap.notMember n items) $ do
859-
modify (HaskMap.insert n (HIGRule $ fmap callToDef r))
882+
when (OMap.notMember n items) $ do
883+
modify (OMap.|> (n, HIGRule $ fmap callToDef r))
860884
goT0 (body g)
861885
-- Note that the parameters here may be different, so this doesn't live
862886
-- under the guard
@@ -890,7 +914,7 @@ toCDDL' mkPseudoRoot hdl =
890914
then (toTopLevelPseudoRoot (roots hdl) NE.<|)
891915
else id
892916
)
893-
$ fmap toCDDLItem (NE.fromList $ items hdl)
917+
$ fmap toCDDLItem (NE.fromList $ fmap (view _2) $ toList $ items hdl)
894918
where
895919
toCDDLItem (HIRule r) = toCDDLRule r
896920
toCDDLItem (HIGroup g) = toCDDLGroup g

src/Codec/CBOR/Cuddle/Huddle/HuddleM.hs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ import Codec.CBOR.Cuddle.Huddle qualified as Huddle
1717
import Control.Monad.State.Strict (State, modify, runState)
1818
import Data.Default.Class (def)
1919
import Data.Generics.Product (HasField (..))
20+
import Data.Map.Ordered.Strict qualified as OMap
2021
import Data.Text qualified as T
21-
import Optics.Core (Field2 (..), set, (%), (%~))
22+
import Optics.Core (set, (%~), (^.))
2223

2324
type HuddleM = State Huddle
2425

@@ -54,22 +55,30 @@ huddleDef :: HuddleM a -> Huddle
5455
huddleDef = snd . huddleDef'
5556

5657
huddleDef' :: HuddleM a -> (a, Huddle)
57-
huddleDef' mh = (_2 % field @"items") %~ reverse $ runState mh def
58+
huddleDef' mh = runState mh def
5859

5960
class Includable a where
6061
-- | Include a rule, group, or generic rule defined elsewhere
6162
include :: a -> HuddleM a
6263

6364
instance Includable Rule where
64-
include r = modify (field @"items" %~ (HIRule r :)) >> pure r
65+
include r =
66+
modify (field @"items" %~ (OMap.|> (r ^. field @"name", HIRule r)))
67+
>> pure r
6568

6669
instance Includable (Named Group) where
67-
include r = modify ((field @"items") %~ (HIGroup r :)) >> pure r
70+
include r =
71+
modify
72+
( (field @"items")
73+
%~ (OMap.|> (r ^. field @"name", HIGroup r))
74+
)
75+
>> pure r
6876

6977
instance (IsType0 t0) => Includable (t0 -> GRuleCall) where
7078
include gr =
7179
let fakeT0 = error "Attempting to unwrap fake value in generic call"
7280
grDef = callToDef <$> gr fakeT0
81+
n = grDef ^. field @"name"
7382
in do
74-
modify (field @"items" %~ (HIGRule grDef :))
83+
modify (field @"items" %~ (OMap.|> (n, HIGRule grDef)))
7584
pure gr

test/Test/Codec/CBOR/Cuddle/Huddle.hs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
module Test.Codec.CBOR.Cuddle.Huddle where
88

9-
import Codec.CBOR.Cuddle.CDDL (CDDL)
9+
import Codec.CBOR.Cuddle.CDDL (CDDL, sortCDDL)
1010
import Codec.CBOR.Cuddle.Huddle
1111
import Codec.CBOR.Cuddle.Parser
1212
import Data.Text qualified as T
@@ -26,37 +26,37 @@ huddleSpec = describe "huddle" $ do
2626
basicAssign :: Spec
2727
basicAssign = describe "basic assignment" $ do
2828
it "Can assign a primitive" $
29-
toCDDLNoRoot ["port" =:= VUInt]
29+
toSortedCDDL ["port" =:= VUInt]
3030
`shouldMatchParseCDDL` "port = uint"
3131
it "Can assign an int" $
32-
toCDDLNoRoot ["one" =:= (int 1)]
32+
toSortedCDDL ["one" =:= (int 1)]
3333
`shouldMatchParseCDDL` "one = 1"
3434
-- it "Can assign a float" $
35-
-- toCDDLNoRoot ["onepointone" =:= (1.1 :: Float)]
35+
-- toSortedCDDL ["onepointone" =:= (1.1 :: Float)]
3636
-- `shouldMatchParseCDDL` "onepointone = 1.1"
3737
it "Can assign a text string" $
38-
toCDDLNoRoot ["hello" =:= ("Hello World" :: T.Text)]
38+
toSortedCDDL ["hello" =:= ("Hello World" :: T.Text)]
3939
`shouldMatchParseCDDL` "hello = \"Hello World\""
4040
it "Can handle multiple assignments" $
41-
toCDDLNoRoot ["age" =:= VUInt, "location" =:= VText]
41+
toSortedCDDL ["age" =:= VUInt, "location" =:= VText]
4242
`shouldMatchParseCDDL` "age = uint\n location = text"
4343

4444
arraySpec :: Spec
4545
arraySpec = describe "Arrays" $ do
4646
it "Can assign a small array" $
47-
toCDDLNoRoot ["asl" =:= arr [a VUInt, a VBool, a VText]]
47+
toSortedCDDL ["asl" =:= arr [a VUInt, a VBool, a VText]]
4848
`shouldMatchParseCDDL` "asl = [ uint, bool, text ]"
4949
it "Can quantify an upper bound" $
50-
toCDDLNoRoot ["age" =:= arr [a VUInt +> 64]]
50+
toSortedCDDL ["age" =:= arr [a VUInt +> 64]]
5151
`shouldMatchParseCDDL` "age = [ *64 uint ]"
5252
it "Can quantify an optional" $
53-
toCDDLNoRoot ["age" =:= arr [0 <+ a VUInt +> 1]]
53+
toSortedCDDL ["age" =:= arr [0 <+ a VUInt +> 1]]
5454
`shouldMatchParseCDDL` "age = [ ? uint ]"
5555
it "Can handle a choice" $
56-
toCDDLNoRoot ["ageOrSex" =:= arr [a VUInt] / arr [a VBool]]
56+
toSortedCDDL ["ageOrSex" =:= arr [a VUInt] / arr [a VBool]]
5757
`shouldMatchParseCDDL` "ageOrSex = [ uint // bool ]"
5858
it "Can handle choices of groups" $
59-
toCDDLNoRoot
59+
toSortedCDDL
6060
[ "asl"
6161
=:= arr [a VUInt, a VBool, a VText]
6262
/ arr
@@ -69,31 +69,31 @@ arraySpec = describe "Arrays" $ do
6969
mapSpec :: Spec
7070
mapSpec = describe "Maps" $ do
7171
it "Can assign a small map" $
72-
toCDDLNoRoot ["asl" =:= mp ["age" ==> VUInt, "sex" ==> VBool, "location" ==> VText]]
72+
toSortedCDDL ["asl" =:= mp ["age" ==> VUInt, "sex" ==> VBool, "location" ==> VText]]
7373
`shouldMatchParseCDDL` "asl = { age : uint, sex : bool, location : text }"
7474
it "Can quantify a lower bound" $
75-
toCDDLNoRoot ["age" =:= mp [0 <+ "years" ==> VUInt]]
75+
toSortedCDDL ["age" =:= mp [0 <+ "years" ==> VUInt]]
7676
`shouldMatchParseCDDL` "age = { * years : uint }"
7777
it "Can quantify an upper bound" $
78-
toCDDLNoRoot ["age" =:= mp ["years" ==> VUInt +> 64]]
78+
toSortedCDDL ["age" =:= mp ["years" ==> VUInt +> 64]]
7979
`shouldMatchParseCDDL` "age = { *64 years : uint }"
8080
it "Can handle a choice" $
81-
toCDDLNoRoot ["ageOrSex" =:= mp ["age" ==> VUInt] / mp ["sex" ==> VBool]]
81+
toSortedCDDL ["ageOrSex" =:= mp ["age" ==> VUInt] / mp ["sex" ==> VBool]]
8282
`shouldMatchParseCDDL` "ageOrSex = { age : uint // sex : bool }"
8383
it "Can handle a choice with an entry" $
84-
toCDDLNoRoot ["mir" =:= arr [a (int 0 / int 1), a $ mp [0 <+ "test" ==> VUInt]]]
84+
toSortedCDDL ["mir" =:= arr [a (int 0 / int 1), a $ mp [0 <+ "test" ==> VUInt]]]
8585
`shouldMatchParseCDDL` "mir = [ 0 / 1, { * test : uint }]"
8686

8787
nestedSpec :: Spec
8888
nestedSpec =
8989
describe "Nesting" $
9090
it "Handles references" $
9191
let headerBody = "header_body" =:= arr ["block_number" ==> VUInt, "slot" ==> VUInt]
92-
in toCDDLNoRoot
92+
in toSortedCDDL
9393
[ headerBody,
9494
"header" =:= arr [a headerBody, "body_signature" ==> VBytes]
9595
]
96-
`shouldMatchParseCDDL` "header_body = [block_number : uint, slot : uint]\n header = [header_body, body_signature : bytes]"
96+
`shouldMatchParseCDDL` "header = [header_body, body_signature : bytes]\n header_body = [block_number : uint, slot : uint]"
9797

9898
genericSpec :: Spec
9999
genericSpec =
@@ -105,11 +105,11 @@ genericSpec =
105105
dict = binding2 $ \k v -> "dict" =:= mp [0 <+ asKey k ==> v]
106106
in do
107107
it "Should bind a single parameter" $
108-
toCDDLNoRoot (collectFrom ["intset" =:= set VUInt])
108+
toSortedCDDL (collectFrom ["intset" =:= set VUInt])
109109
`shouldMatchParseCDDL` "intset = set<uint>\n set<a0> = [* a0]"
110110
it "Should bind two parameters" $
111-
toCDDLNoRoot (collectFrom ["mymap" =:= dict VUInt VText])
112-
`shouldMatchParseCDDL` "mymap = dict<uint, text>\n dict<a0, b0> = {* a0 => b0}"
111+
toSortedCDDL (collectFrom ["mymap" =:= dict VUInt VText])
112+
`shouldMatchParseCDDL` "dict<a0, b0> = {* a0 => b0}\n mymap = dict<uint, text>"
113113

114114
--------------------------------------------------------------------------------
115115
-- Helper functions
@@ -128,3 +128,6 @@ shouldMatchParseCDDL ::
128128
String ->
129129
Expectation
130130
shouldMatchParseCDDL x = shouldMatchParse x pCDDL
131+
132+
toSortedCDDL :: Huddle -> CDDL
133+
toSortedCDDL = sortCDDL . toCDDLNoRoot

0 commit comments

Comments
 (0)