Skip to content

Commit be1e3d0

Browse files
authored
Merge pull request #28 from input-output-hk/nc/hex-str
Generation improvements
2 parents e6f34fc + fe15cf2 commit be1e3d0

File tree

7 files changed

+106
-41
lines changed

7 files changed

+106
-41
lines changed

cuddle.cabal

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

66
-- description:
@@ -58,6 +58,7 @@ library
5858
-- other-extensions:
5959
build-depends:
6060
, base ^>=4.14.3.0 || ^>=4.16.3.0 || ^>=4.18.1.0 || ^>=4.19.0.0
61+
, base16-bytestring
6162
, bytestring
6263
, capability
6364
, cborg

example/Conway.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import GHC.Show (Show (show))
2020
default (Integer, Double)
2121

2222
conway :: Huddle
23-
conway = collectFrom block
23+
conway = collectFrom [block]
2424

2525
block :: Rule
2626
block =

src/Codec/CBOR/Cuddle/CBOR/Gen.hs

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
{-# OPTIONS_GHC -Wno-unticked-promoted-constructors #-}
1212

1313
-- | Generate example CBOR given a CDDL specification
14-
module Codec.CBOR.Cuddle.CBOR.Gen where
14+
module Codec.CBOR.Cuddle.CBOR.Gen (generateCBORTerm, generateCBORTerm') where
1515

1616
import Capability.Reader
1717
import Capability.Sink (HasSink)
1818
import Capability.Source (HasSource, MonadState (..))
19-
import Capability.State (HasState, state)
19+
import Capability.State (HasState, get, modify, state)
2020
import Codec.CBOR.Cuddle.CDDL
2121
( Name (..),
2222
OccurrenceIndicator (..),
@@ -28,37 +28,39 @@ import Codec.CBOR.Cuddle.CDDL.CtlOp qualified as CtlOp
2828
import Codec.CBOR.Cuddle.CDDL.Postlude (PTerm (..))
2929
import Codec.CBOR.Cuddle.CDDL.Resolve (MonoRef (..))
3030
import Codec.CBOR.Term (Term (..))
31-
import Control.Monad (replicateM, (<=<))
31+
import Codec.CBOR.Term qualified as CBOR
32+
import Codec.CBOR.Write qualified as CBOR
33+
import Control.Monad (join, replicateM, (<=<))
3234
import Control.Monad.Reader (Reader, runReader)
3335
import Control.Monad.State.Strict (StateT, runStateT)
3436
import Data.ByteString (ByteString)
37+
import Data.ByteString.Base16 qualified as Base16
3538
import Data.Functor ((<&>))
3639
import Data.Functor.Identity (Identity (runIdentity))
40+
import Data.List (foldl')
3741
import Data.List.NonEmpty qualified as NE
3842
import Data.Map.Strict qualified as Map
3943
import Data.Maybe (fromMaybe)
4044
import Data.Text (Text)
4145
import Data.Text qualified as T
42-
import Data.Word (Word64)
46+
import Data.Word (Word32, Word64)
4347
import GHC.Generics (Generic)
4448
import System.Random.Stateful
4549
( Random,
4650
RandomGen (genShortByteString, genWord32, genWord64),
4751
RandomGenM,
4852
StatefulGen (..),
49-
StdGen,
5053
UniformRange (uniformRM),
5154
applyRandomGenM,
5255
randomM,
5356
uniformByteStringM,
5457
)
58+
import Data.Bifunctor (second)
5559

5660
--------------------------------------------------------------------------------
5761
-- Generator infrastructure
5862
--------------------------------------------------------------------------------
5963

60-
type TypeMap = Map.Map Name (Gen Term)
61-
6264
-- | Generator context, parametrised over the type of the random seed
6365
data GenEnv g = GenEnv
6466
{ cddl :: CTreeRoot' Identity MonoRef,
@@ -67,9 +69,14 @@ data GenEnv g = GenEnv
6769
}
6870
deriving (Generic)
6971

70-
newtype GenState g = GenState
72+
data GenState g = GenState
7173
{ -- | Actual seed
72-
randomSeed :: g
74+
randomSeed :: g,
75+
-- | Depth of the generator. This measures the number of references we
76+
-- follow. As we go deeper into the tree, we try to reduce the likelihood of
77+
-- following recursive paths, and generate shorter lists where allowed by
78+
-- the occurrence bounds.
79+
depth :: Int
7380
}
7481
deriving (Generic)
7582

@@ -81,6 +88,12 @@ newtype M g a = M {runM :: StateT (GenState g) (Reader (GenEnv g)) a}
8188
"randomSeed"
8289
()
8390
(MonadState (StateT (GenState g) (Reader (GenEnv g))))
91+
deriving
92+
(HasSource "depth" Int, HasSink "depth" Int, HasState "depth" Int)
93+
via Field
94+
"depth"
95+
()
96+
(MonadState (StateT (GenState g) (Reader (GenEnv g))))
8497
deriving
8598
( HasSource "cddl" (CTreeRoot' Identity MonoRef),
8699
HasReader "cddl" (CTreeRoot' Identity MonoRef)
@@ -109,8 +122,6 @@ instance (RandomGen g) => StatefulGen (CapGenM g) (M g) where
109122
instance (RandomGen r) => RandomGenM (CapGenM r) r (M r) where
110123
applyRandomGenM f _ = state @"randomSeed" f
111124

112-
type Gen = M StdGen
113-
114125
runGen :: M g a -> GenEnv g -> GenState g -> (a, GenState g)
115126
runGen (M m) env st = runReader (runStateT m st) env
116127

@@ -127,40 +138,59 @@ asksM f = f =<< ask @tag
127138
genUniformRM :: forall a g. (UniformRange a, RandomGen g) => (a, a) -> M g a
128139
genUniformRM = asksM @"fakeSeed" . uniformRM
129140

141+
-- | Generate a random number in a given range, biased increasingly towards the
142+
-- lower end as the depth parameter increases.
143+
genDepthBiasedRM ::
144+
forall a g.
145+
(Ord a, UniformRange a, RandomGen g) =>
146+
(a, a) ->
147+
M g a
148+
genDepthBiasedRM bounds = do
149+
fs <- ask @"fakeSeed"
150+
d <- get @"depth"
151+
samples <- replicateM d (uniformRM bounds fs)
152+
pure $ minimum samples
153+
154+
-- | Generates a bool, increasingly likely to be 'False' as the depth increases.
155+
genDepthBiasedBool :: forall g. (RandomGen g) => M g Bool
156+
genDepthBiasedBool = do
157+
d <- get @"depth"
158+
foldl' (&&) True <$> replicateM d genRandomM
159+
130160
genRandomM :: forall g a. (Random a, RandomGen g) => M g a
131161
genRandomM = asksM @"fakeSeed" randomM
132162

133163
genBytes :: forall g. (RandomGen g) => Int -> M g ByteString
134164
genBytes n = asksM @"fakeSeed" $ uniformByteStringM n
135165

136166
genText :: forall g. (RandomGen g) => Int -> M g Text
137-
genText n = pure $ T.pack $ take n ['a' ..]
167+
genText n = pure $ T.pack . take n . join $ repeat ['a' .. 'z']
138168

139169
--------------------------------------------------------------------------------
140170
-- Combinators
141171
--------------------------------------------------------------------------------
142172

143-
choose :: [a] -> Gen a
173+
choose :: (RandomGen g) => [a] -> M g a
144174
choose xs = genUniformRM (0, length xs) >>= \i -> pure $ xs !! i
145175

146-
oneOf :: [Gen a] -> Gen a
176+
oneOf :: (RandomGen g) => [M g a] -> M g a
147177
oneOf xs = genUniformRM (0, length xs) >>= \i -> xs !! i
148178

149-
oneOfGenerated :: Gen [a] -> Gen a
179+
oneOfGenerated :: (RandomGen g) => M g [a] -> M g a
150180
oneOfGenerated genXs = genXs >>= choose
151181

152182
--------------------------------------------------------------------------------
153183
-- Postlude
154184
--------------------------------------------------------------------------------
155185

156186
-- | Primitive types defined by the CDDL specification, with their generators
157-
genPostlude :: PTerm -> Gen Term
187+
genPostlude :: (RandomGen g) => PTerm -> M g Term
158188
genPostlude pt = case pt of
159189
PTBool ->
160190
genRandomM
161191
<&> TBool
162192
PTUInt ->
163-
genUniformRM (minBound :: Word, maxBound)
193+
genUniformRM (minBound :: Word32, maxBound)
164194
<&> TInteger
165195
. fromIntegral
166196
PTNInt ->
@@ -232,7 +262,7 @@ pattern G xs = GroupTerm xs
232262
-- Generator functions
233263
--------------------------------------------------------------------------------
234264

235-
genForCTree :: CTree MonoRef -> Gen WrappedTerm
265+
genForCTree :: (RandomGen g) => CTree MonoRef -> M g WrappedTerm
236266
genForCTree (CTree.Literal v) = S <$> genValue v
237267
genForCTree (CTree.Postlude pt) = S <$> genPostlude pt
238268
genForCTree (CTree.Map nodes) = do
@@ -278,6 +308,14 @@ genForCTree (CTree.Control op target controller) = do
278308
tt <- resolveIfRef target
279309
ct <- resolveIfRef controller
280310
case (op, ct) of
311+
(CtlOp.Le, CTree.Literal (VUInt n)) -> case tt of
312+
CTree.Postlude PTUInt -> S. TInteger <$> genUniformRM (0, fromIntegral n)
313+
_ -> error "Cannot apply le operator to target"
314+
(CtlOp.Le, _) -> error $ "Invalid controller for .le operator: " <> show controller
315+
(CtlOp.Lt, CTree.Literal (VUInt n)) -> case tt of
316+
CTree.Postlude PTUInt -> S. TInteger <$> genUniformRM (0, fromIntegral n - 1)
317+
_ -> error "Cannot apply lt operator to target"
318+
(CtlOp.Lt, _) -> error $ "Invalid controller for .lt operator: " <> show controller
281319
(CtlOp.Size, CTree.Literal (VUInt n)) -> case tt of
282320
CTree.Postlude PTText -> S . TString <$> genText (fromIntegral n)
283321
CTree.Postlude PTBytes -> S . TBytes <$> genBytes (fromIntegral n)
@@ -306,6 +344,11 @@ genForCTree (CTree.Control op target controller) = do
306344
error $
307345
"Invalid controller for .size operator: "
308346
<> show controller
347+
(CtlOp.Cbor, _) -> do
348+
enc <- genForCTree ct
349+
case enc of
350+
S x -> pure . S . TBytes . CBOR.toStrictByteString $ CBOR.encodeTerm x
351+
_ -> error "Controller does not correspond to a single term"
309352
_ -> genForNode target
310353
genForCTree (CTree.Enum node) = do
311354
tree <- resolveIfRef node
@@ -315,18 +358,25 @@ genForCTree (CTree.Enum node) = do
315358
genForNode $ nodes !! ix
316359
_ -> error "Attempt to form an enum from something other than a group"
317360
genForCTree (CTree.Unwrap node) = genForCTree =<< resolveIfRef node
361+
genForCTree (CTree.Tag tag node) = do
362+
enc <- genForNode node
363+
case enc of
364+
S x -> pure $ S $ TTagged tag x
365+
_ -> error "Tag controller does not correspond to a single term"
318366

319-
genForNode :: CTree.Node MonoRef -> Gen WrappedTerm
367+
genForNode :: (RandomGen g) => CTree.Node MonoRef -> M g WrappedTerm
320368
genForNode = genForCTree <=< resolveIfRef
321369

322370
-- | Take something which might be a reference and resolve it to the relevant
323371
-- Tree, following multiple links if necessary.
324-
resolveIfRef :: CTree.Node MonoRef -> Gen (CTree MonoRef)
372+
resolveIfRef :: (RandomGen g) => CTree.Node MonoRef -> M g (CTree MonoRef)
325373
resolveIfRef (MIt a) = pure a
326374
resolveIfRef (MRuleRef n) = do
327375
(CTreeRoot cddl) <- ask @"cddl"
376+
-- Since we follow a reference, we increase the 'depth' of the gen monad.
377+
modify @"depth" (+ 1)
328378
case Map.lookup n cddl of
329-
Nothing -> error "Unbound reference"
379+
Nothing -> error $ "Unbound reference: " <> show n
330380
Just val -> resolveIfRef $ runIdentity val
331381

332382
-- | Generate a CBOR Term corresponding to a top-level name.
@@ -337,11 +387,11 @@ resolveIfRef (MRuleRef n) = do
337387
-- This will throw an error if the generated item does not correspond to a
338388
-- single CBOR term (e.g. if the name resolves to a group, which cannot be
339389
-- generated outside a context).
340-
genForName :: Name -> Gen Term
390+
genForName :: (RandomGen g) => Name -> M g Term
341391
genForName n = do
342392
(CTreeRoot cddl) <- ask @"cddl"
343393
case Map.lookup n cddl of
344-
Nothing -> error "Unbound reference"
394+
Nothing -> error $ "Unbound reference: " <> show n
345395
Just val ->
346396
genForNode (runIdentity val) >>= \case
347397
S x -> pure x
@@ -353,39 +403,48 @@ genForName n = do
353403

354404
-- | Apply an occurence indicator to a group entry
355405
applyOccurenceIndicator ::
406+
(RandomGen g) =>
356407
OccurrenceIndicator ->
357-
Gen WrappedTerm ->
358-
Gen WrappedTerm
408+
M g WrappedTerm ->
409+
M g WrappedTerm
359410
applyOccurenceIndicator OIOptional oldGen =
360-
genRandomM >>= \case
411+
genDepthBiasedBool >>= \case
361412
False -> pure $ G mempty
362413
True -> oldGen
363414
applyOccurenceIndicator OIZeroOrMore oldGen =
364-
genUniformRM (0 :: Int, 10) >>= \i ->
415+
genDepthBiasedRM (0 :: Int, 10) >>= \i ->
365416
G <$> replicateM i oldGen
366417
applyOccurenceIndicator OIOneOrMore oldGen =
367-
genUniformRM (0 :: Int, 10) >>= \i ->
418+
genDepthBiasedRM (1 :: Int, 10) >>= \i ->
368419
G <$> replicateM i oldGen
369420
applyOccurenceIndicator (OIBounded mlb mub) oldGen =
370-
genUniformRM (fromMaybe 0 mlb :: Word64, fromMaybe 10 mub)
421+
genDepthBiasedRM (fromMaybe 0 mlb :: Word64, fromMaybe 10 mub)
371422
>>= \i -> G <$> replicateM (fromIntegral i) oldGen
372423

373-
genValue :: Value -> Gen Term
424+
genValue :: (RandomGen g) => Value -> M g Term
374425
genValue (VUInt i) = pure . TInt $ fromIntegral i
375426
genValue (VNInt i) = pure . TInt $ fromIntegral (-i)
376427
genValue (VBignum i) = pure $ TInteger i
377428
genValue (VFloat16 i) = pure . THalf $ i
378429
genValue (VFloat32 i) = pure . TFloat $ i
379430
genValue (VFloat64 i) = pure . TDouble $ i
380431
genValue (VText t) = pure $ TString t
381-
genValue (VBytes b) = pure $ TBytes b
432+
genValue (VBytes b) = case Base16.decode b of
433+
Right bHex -> pure $ TBytes bHex
434+
Left err -> error $ "Unable to parse hex encoded bytestring: " <> err
382435

383436
--------------------------------------------------------------------------------
384437
-- Generator functions
385438
--------------------------------------------------------------------------------
386439

387-
generateCBORTerm :: CTreeRoot' Identity MonoRef -> Name -> StdGen -> Term
440+
generateCBORTerm :: (RandomGen g) => CTreeRoot' Identity MonoRef -> Name -> g -> Term
388441
generateCBORTerm cddl n stdGen =
389442
let genEnv = GenEnv {cddl, fakeSeed = CapGenM}
390-
genState = GenState {randomSeed = stdGen}
443+
genState = GenState {randomSeed = stdGen, depth = 1}
391444
in evalGen (genForName n) genEnv genState
445+
446+
generateCBORTerm' :: (RandomGen g) => CTreeRoot' Identity MonoRef -> Name -> g -> (Term, g)
447+
generateCBORTerm' cddl n stdGen =
448+
let genEnv = GenEnv {cddl, fakeSeed = CapGenM}
449+
genState = GenState {randomSeed = stdGen, depth = 1}
450+
in second randomSeed $ runGen (genForName n) genEnv genState

src/Codec/CBOR/Cuddle/CDDL/CTree.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Codec.CBOR.Cuddle.CDDL.Postlude (PTerm)
1313
import Data.Hashable (Hashable)
1414
import Data.List.NonEmpty qualified as NE
1515
import Data.Map.Strict qualified as Map
16+
import Data.Word (Word64)
1617
import GHC.Generics (Generic)
1718

1819
--------------------------------------------------------------------------------
@@ -42,6 +43,7 @@ data CTree f
4243
| Control {op :: CtlOp, target :: Node f, controller :: Node f}
4344
| Enum (Node f)
4445
| Unwrap (Node f)
46+
| Tag Word64 (Node f)
4547
deriving (Generic)
4648

4749
-- | Traverse the CTree, carrying out the given operation at each node
@@ -67,6 +69,7 @@ traverseCTree atNode (Control o t c) = do
6769
pure $ Control o t' c'
6870
traverseCTree atNode (Enum ref) = Enum <$> atNode ref
6971
traverseCTree atNode (Unwrap ref) = Unwrap <$> atNode ref
72+
traverseCTree atNode (Tag i ref) = Tag i <$> atNode ref
7073

7174
type Node f = f (CTree f)
7275

src/Codec/CBOR/Cuddle/CDDL/Resolve.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,11 @@ buildRefCTree rules = CTreeRoot $ fmap toCTreeRule rules
183183
Ref n (fromGenArgs margs)
184184
toCTreeT2 (T2Enum g) = toCTreeEnum g
185185
toCTreeT2 (T2EnumRef n margs) = Ref n $ fromGenArgs margs
186-
toCTreeT2 (T2Tag _mint t0) =
186+
toCTreeT2 (T2Tag Nothing t0) =
187187
-- Currently not validating tags
188188
toCTreeT0 t0
189+
toCTreeT2 (T2Tag (Just tag) t0) =
190+
It . CTree.Tag tag $ toCTreeT0 t0
189191
toCTreeT2 (T2DataItem _maj _mmin) =
190192
-- We don't validate numerical items yet
191193
It $ CTree.Postlude PTAny

src/Codec/CBOR/Cuddle/Huddle.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -789,11 +789,11 @@ binding2 fRule t0 t1 =
789789
--------------------------------------------------------------------------------
790790

791791
-- | Collect all rules starting from a given point.
792-
collectFrom :: Rule -> Huddle
793-
collectFrom topR =
792+
collectFrom :: [Rule] -> Huddle
793+
collectFrom topRs =
794794
toHuddle $
795795
execState
796-
(goRule topR)
796+
(traverse goRule topRs)
797797
(HaskMap.empty, HaskMap.empty, HaskMap.empty)
798798
where
799799
toHuddle (rules, groups, gRules) =

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ genericSpec =
105105
dict = binding2 $ \k v -> "dict" =:= mp [0 <+ asKey k ==> v]
106106
in do
107107
it "Should bind a single parameter" $
108-
toCDDL (collectFrom ("intset" =:= set VUInt))
108+
toCDDL (collectFrom ["intset" =:= set VUInt])
109109
`shouldMatchParseCDDL` "intset = set<uint>\n set<a0> = [* a0]"
110110
it "Should bind two parameters" $
111-
toCDDL (collectFrom ("mymap" =:= dict VUInt VText))
111+
toCDDL (collectFrom ["mymap" =:= dict VUInt VText])
112112
`shouldMatchParseCDDL` "mymap = dict<uint, text>\n dict<a0, b0> = {* a0 => b0}"
113113

114114
--------------------------------------------------------------------------------

0 commit comments

Comments
 (0)