packages feed

urbit-hob 0.2.0 → 0.3.0

raw patch · 10 files changed

+502/−61 lines, 10 filesdep +criteriondep +deepseqdep ~base

Dependencies added: criterion, deepseq

Dependency ranges changed: base

Files

+ bench/Ob.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import Control.DeepSeq+import Criterion.Main+import qualified Urbit.Ob as Ob++instance NFData Ob.Patp++patpGroup :: Benchmark+patpGroup = bgroup "patp" [+    bench "~zod" $+      nf Ob.patp 0x0000++  , bench "~marzod" $+      nf Ob.patp 0x0100++  , bench "~dapnep-ronmyl" $+      nf Ob.patp 0x00010000++  , bench "~fipfes-fipfes-dostec-risfen" $+      nf Ob.patp 0xFFFFFFFFFFFFFFFF++  , bench "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes" $+      nf Ob.patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF+  ]++fromPatpGroup :: Benchmark+fromPatpGroup = bgroup "fromPatp" [+   bench "~zod" $+     whnf Ob.fromPatp (Ob.patp 0x0000)++ , bench "~marzod" $+     whnf Ob.fromPatp (Ob.patp 0x0100)++ , bench "~dapnep-ronmyl" $+     whnf Ob.fromPatp (Ob.patp 0x00010000)++ , bench "~fipfes-fipfes-dostec-risfen" $+     whnf Ob.fromPatp (Ob.patp 0xFFFFFFFFFFFFFFFF)++ , bench "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes" $+     whnf Ob.fromPatp (Ob.patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)+ ]++renderGroup :: Benchmark+renderGroup = bgroup "renderPatp" [+   bench "~zod" $+     nf Ob.renderPatp (Ob.patp 0x0000)++ , bench "~marzod" $+     nf Ob.renderPatp (Ob.patp 0x0100)++ , bench "~dapnep-ronmyl" $+     nf Ob.renderPatp (Ob.patp 0x00010000)++ , bench "~fipfes-fipfes-dostec-risfen" $+     nf Ob.renderPatp (Ob.patp 0xFFFFFFFFFFFFFFFF)++ , bench "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes" $+     nf Ob.renderPatp (Ob.patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)+ ]++parseGroup :: Benchmark+parseGroup = bgroup "parsePatp" [+   bench "~zod" $+     nf Ob.parsePatp "~zod"++ , bench "~marzod" $+     nf Ob.parsePatp "~marzod"++ , bench "~dapnep-ronmyl" $+     nf Ob.parsePatp "~dapnep-ronmyl"++ , bench "~fipfes-fipfes-dostec-risfen" $+     nf Ob.parsePatp "~fipfes-fipfes-dostec-risfen"++ , bench "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes" $+     nf Ob.parsePatp "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes"+ ]++main :: IO ()+main = defaultMain [+    patpGroup+  , fromPatpGroup+  , renderGroup+  , parseGroup+  ]
lib/Urbit/Ob.hs view
@@ -6,3 +6,4 @@ import Urbit.Ob.Co as E import Urbit.Ob.Muk as E import Urbit.Ob.Ob as E+import Urbit.Ob.Title as E
lib/Urbit/Ob/Co.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}  -- |@@ -15,12 +16,19 @@  module Urbit.Ob.Co (     Patp+  , Patq    , patp+  , patq+   , fromPatp+  , fromPatq -  , render-  , parse+  , renderPatp+  , renderPatq++  , parsePatp+  , parsePatq   ) where  import qualified Data.ByteString as BS@@ -30,6 +38,7 @@ import qualified Data.Text as T import qualified Data.Vector as V import Data.Word (Word8)+import GHC.Generics (Generic) import Numeric.Natural (Natural) import Prelude hiding (log) import qualified Urbit.Ob.Ob as Ob (fein, fynd)@@ -44,15 +53,28 @@ -- --   (It's also used for naming comets, i.e. self-signed 128-bit Urbit ships.) ---newtype Patp = Patp BS.ByteString-  deriving Eq+newtype Patp = Patp {+    unPatp :: BS.ByteString+  } deriving (Eq, Generic)  instance Show Patp where-  show = T.unpack . render+  show = T.unpack . renderPatp -unPatp :: Patp -> BS.ByteString-unPatp (Patp p) = p+-- | Hoon's \@q encoding.+--+--   Unlike \@p, the \@q encoding is a /non-obfuscated/ representation of an+--   atom.+--+--   It's typically used for serializing arbitrary data in a memorable and+--   pronounceable fashion.+--+newtype Patq = Patq {+    unPatq :: BS.ByteString+  } deriving (Eq, Generic) +instance Show Patq where+  show = T.unpack . renderPatq+ -- | Convert a 'Natural' to \@p. -- --   >>> patp 0@@ -67,6 +89,20 @@ patp :: Natural -> Patp patp = Patp . BS.reverse . C.unroll . Ob.fein +-- | Convert a 'Natural' to \@q.+--+--   >>> patq 0+--   ~zod+--   >>> patp 256+--   ~marzod+--   >>> patp 65536+--   ~nec-dozzod+--   >>> patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF+--   ~fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes+--+patq :: Natural -> Patq+patq = Patq . BS.reverse . C.unroll+ -- | Convert a \@p value to its corresponding 'Natural'. -- --   >>> let zod = patp 0@@ -76,17 +112,68 @@ fromPatp :: Patp -> Natural fromPatp = Ob.fynd . C.roll . BS.reverse . unPatp +-- | Convert a \@q value to its corresponding 'Natural'.+--+--   >>> let zod = patq 0+--   >>> fromPatq zod+--   0+--+fromPatq :: Patq -> Natural+fromPatq = C.roll . BS.reverse . unPatq+ -- | Render a \@p value as 'T.Text'. -----   >>> render (patp 0)+--   >>> renderPatp (patp 0) --   "~zod"---   >>> render (patp 15663360)+--   >>> renderPatp (patp 15663360) --   "~nidsut-tomdun"-render :: Patp -> T.Text-render (Patp bs) = render' bs+renderPatp :: Patp -> T.Text+renderPatp (Patp bs) = render' Padding LongSpacing bs -render' :: BS.ByteString -> T.Text-render' bs =+-- | Render a \@p value as 'T.Text'.+--+--   >>> renderPatq (patq 0)+--   "~zod"+--   >>> renderPatq (patq 15663360)+--   "~mun-marzod"+renderPatq :: Patq -> T.Text+renderPatq (Patq bs) = render' NoPadding ShortSpacing bs++-- | Parse a \@p value existing as 'T.Text'.+--+--   >>> parsePatp "~nidsut-tomdun"+--   Right ~nidsut-tomdun+--   > parsePatp "~fipfes-fipfes-fipfes-doznec"+--   Right ~fipfes-fipfes-fipfes-doznec+--+parsePatp :: T.Text -> Either T.Text Patp+parsePatp = fmap Patp . parse++-- | Parse a \@q value existing as 'T.Text'.+--+--   >>> parsePatq "~nec-dozzod"+--   Right ~nec-dozzod+--   > parsePatq "~fipfes-fipfes-fipfes-doznec"+--   Right ~fipfes-fipfes-fipfes-doznec+--+parsePatq :: T.Text -> Either T.Text Patq+parsePatq = fmap Patq . parse++-- Padding option for rendering.+data Padding =+    Padding+  | NoPadding+  deriving Eq++-- Spacing option for rendering.+data Spacing =+    LongSpacing+  | ShortSpacing+  deriving Eq++-- General-purpose renderer.+render' :: Padding -> Spacing -> BS.ByteString -> T.Text+render' padding spacing bs =       T.cons '~'     . snd     . BS.foldr alg (0 :: Int, mempty)@@ -95,27 +182,29 @@     alg val (idx, acc) =       let syl = if even idx then suffix val else prefix val           glue-            | idx `mod` 8 == 0 = if idx == 0 then mempty else "--"+            | idx `mod` 8 == 0 = if idx == 0 then mempty else dash             | even idx         = "-"             | otherwise        = mempty       in  (succ idx, syl <> glue <> acc) -    padded =+    padded+      | padCondition = BS.cons 0 bs+      | otherwise    = bs++    dash = case spacing of+      LongSpacing  -> "--"+      ShortSpacing -> "-"++    padCondition =       let len = BS.length bs-      in  if   (odd len && len > 2) || len == 0-          then BS.cons 0 bs-          else bs+      in  case padding of+            NoPadding -> len == 0+            Padding   -> (odd len && len > 2) || len == 0 --- | Parse a \@p value existing as 'T.Text'.------   >>> parse "~nidsut-tomdun"---   Right ~nidsut-tomdun---   > parse "~fipfes-fipfes-fipfes-doznec"---   Right ~fipfes-fipfes-fipfes-doznec----parse :: T.Text -> Either T.Text Patp+-- General-purpose (non-strict) parser.+parse :: T.Text -> Either T.Text BS.ByteString parse p =-      fmap (Patp . snd)+      fmap snd     $ foldrM alg (0 :: Int, mempty) syls   where     alg syl (idx, acc) = do
+ lib/Urbit/Ob/Title.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Urbit.Ob.Title+-- Copyright: (c) 2019 Jared Tobin+-- License: MIT+--+-- Maintainer: Jared Tobin <jared@jtobin.io>+-- Stability: unstable+-- Portability: ghc+--+-- Functions for determining ship class and parentage.+--+-- Analogous to the +title arm of zuse.hoon.++module Urbit.Ob.Title (+    Class(..)+  , clan+  , sein+  ) where++import Urbit.Ob.Co (Patp)+import qualified Urbit.Ob.Co as Co (patp, fromPatp)++-- | Ship class.+data Class =+    -- | 8-bit atom+    --+    Galaxy+    -- | 16-bit atom+    --+  | Star+    -- | 32-bit atom+    --+  | Planet+    -- | 64-bit atom+    --+  | Moon+    -- | 128-bit atom+    --+  | Comet+  deriving (Eq, Show)++-- | Determine ship class.+--+--   >>> let fes = patp 255+--   >>> let fipfes = patp 256+--   >>> let dostec = patp 0xFFFFFFFF+--   >>> let risfen = patp 0xFFFFFFFFFFFFFFFF+--   >>> let fipfesfipfes = patp 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF+--   >>> clan fes+--   Galaxy+--   >>> clan fipfes+--   Star+--   >>> clan dostec+--   Planet+--   >>> clan risfen+--   Moon+--   >>> clan fipfesfipfes+--   Comet+--+clan :: Patp -> Class+clan ship+    | wid <= 1  = Galaxy+    | wid == 2  = Star+    | wid <= 4  = Planet+    | wid <= 8  = Moon+    | otherwise = Comet+  where+    wid = met 3 (Co.fromPatp ship)++-- | Determine parent.+--+--   A ship's parent signs for it on the network.  'sein' establishes the+--   so-called /autoboss/ of a ship (which can escape to another sponsor).+--+--   Note that galaxies sign for themselves, and stars sign for comets.+--+--   >>> sein fes+--   ~fes+--   >>> sein fipfes+--   ~fes+--   >>> sein dostec+--   ~fipfes+--   >>> sein risfen+--   ~dostec-risfen+--   >>> sein fipfesfipfes+--   ~fipfes+--+sein :: Patp -> Patp+sein ship = Co.patp $ case clan ship of+    Galaxy -> nat+    Star   -> end 3 1 nat+    Planet -> end 4 1 nat+    Moon   -> end 5 1 nat+    Comet  -> end 4 1 nat+  where+    nat = Co.fromPatp ship++met :: Integral a => a -> a -> a+met = loop 0 where+  loop !acc a !b+    | b == 0    = acc+    | otherwise = loop (succ acc) a (rsh a 1 b)++rsh :: Integral a => a -> a -> a -> a+rsh a b c = c `div` 2 ^ (2 ^ a * b)++end :: Integral a => a -> a -> a -> a+end a b c = c `mod` 2 ^ (2 ^ a * b)+
test/Co/Tests/Property.hs view
@@ -4,7 +4,7 @@   ) where  import qualified Data.Text as T-import Data.Word (Word32)+import Data.Word (Word64) import Numeric.Natural (Natural) import Test.Hspec import Test.Hspec.Core.QuickCheck (modifyMaxSuccess)@@ -12,14 +12,20 @@ import qualified Urbit.Ob.Co as Co  nats :: Gen Natural-nats = fmap fromIntegral (arbitrary :: Gen Word32)+nats = fmap fromIntegral (arbitrary :: Gen Word64)  patps :: Gen Co.Patp patps = fmap Co.patp nats +patqs :: Gen Co.Patq+patqs = fmap Co.patq nats+ patpStrings :: Gen T.Text-patpStrings = fmap Co.render patps+patpStrings = fmap Co.renderPatp patps +patqStrings :: Gen T.Text+patqStrings = fmap Co.renderPatq patqs+ tests :: Spec tests = do   describe "fromPatp" $@@ -32,11 +38,29 @@       it "inverts fromPatp" $         forAll patps $ \x -> Co.patp (Co.fromPatp x) == x -  describe "render" $+  describe "renderPatp" $     modifyMaxSuccess (const 1000) $-      it "inverts parse" $+      it "inverts parsePatp" $         forAll patpStrings $ \x ->-          case Co.parse x of+          case Co.parsePatp x of             Left _  -> False-            Right p -> Co.render p == x+            Right p -> Co.renderPatp p == x++  describe "fromPatq" $+    modifyMaxSuccess (const 1000) $+      it "inverts patq" $+        forAll nats $ \x -> Co.fromPatq (Co.patq x) == x++  describe "patq" $+    modifyMaxSuccess (const 1000) $+      it "inverts fromPatq" $+        forAll patqs $ \x -> Co.patq (Co.fromPatq x) == x++  describe "renderPatq" $+    modifyMaxSuccess (const 1000) $+      it "inverts parsePatq" $+        forAll patqStrings $ \x ->+          case Co.parsePatq x of+            Left _  -> False+            Right p -> Co.renderPatq p == x 
test/Co/Tests/Unit.hs view
@@ -9,32 +9,61 @@ import qualified Urbit.Ob.Co as Co  tests :: Spec-tests =-  describe "render" $ do+tests = do+  describe "renderPatp" $ do     it "matches 32-bit reference values" $ do-      Co.render (Co.patp 0) `shouldBe` "~zod"-      Co.render (Co.patp 255) `shouldBe` "~fes"-      Co.render (Co.patp 256) `shouldBe` "~marzod"-      Co.render (Co.patp 65535) `shouldBe` "~fipfes"-      Co.render (Co.patp 65536) `shouldBe` "~dapnep-ronmyl"-      Co.render (Co.patp 15663360) `shouldBe` "~nidsut-tomdun"-      Co.render (Co.patp 0xFFFFFFFF) `shouldBe` "~dostec-risfen"+      Co.renderPatp (Co.patp 0) `shouldBe` "~zod"+      Co.renderPatp (Co.patp 255) `shouldBe` "~fes"+      Co.renderPatp (Co.patp 256) `shouldBe` "~marzod"+      Co.renderPatp (Co.patp 65535) `shouldBe` "~fipfes"+      Co.renderPatp (Co.patp 65536) `shouldBe` "~dapnep-ronmyl"+      Co.renderPatp (Co.patp 15663360) `shouldBe` "~nidsut-tomdun"+      Co.renderPatp (Co.patp 0xFFFFFFFF) `shouldBe` "~dostec-risfen"      it "matches 64-bit reference values" $ do       let big_64_01 = 0x0000000100000000-      Co.render (Co.patp big_64_01) `shouldBe` "~doznec-dozzod-dozzod"+      Co.renderPatp (Co.patp big_64_01) `shouldBe` "~doznec-dozzod-dozzod"        let big_64_02 = 0xFFFFFFFFFFFFFFFF-      Co.render (Co.patp big_64_02) `shouldBe` "~fipfes-fipfes-dostec-risfen"+      Co.renderPatp (Co.patp big_64_02) `shouldBe` "~fipfes-fipfes-dostec-risfen"      it "matches 128-bit reference values" $ do       let big_128_01  = 0x00000000000000010000000000000000           patp_128_01 = "~doznec--dozzod-dozzod-dozzod-dozzod"-      Co.render (Co.patp big_128_01) `shouldBe` patp_128_01+      Co.renderPatp (Co.patp big_128_01) `shouldBe` patp_128_01        let big_128_02  = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF           patp_128_02 =             "~fipfes-fipfes-fipfes-fipfes--fipfes-fipfes-fipfes-fipfes" -      Co.render (Co.patp big_128_02) `shouldBe` patp_128_02+      Co.renderPatp (Co.patp big_128_02) `shouldBe` patp_128_02++  describe "renderPatq" $ do+    it "matches 32-bit reference values" $ do+      Co.renderPatq (Co.patq 0) `shouldBe` "~zod"+      Co.renderPatq (Co.patq 255) `shouldBe` "~fes"+      Co.renderPatq (Co.patq 256) `shouldBe` "~marzod"+      Co.renderPatq (Co.patq 65535) `shouldBe` "~fipfes"+      Co.renderPatq (Co.patq 65536) `shouldBe` "~nec-dozzod"+      Co.renderPatq (Co.patq 15663360) `shouldBe` "~mun-marzod"+      Co.renderPatq (Co.patq 0xFFFFFFFF) `shouldBe` "~fipfes-fipfes"++    it "matches 64-bit reference values" $ do+      let big_64_01 = 0x0000000100000000+      Co.renderPatq (Co.patq big_64_01) `shouldBe` "~nec-dozzod-dozzod"++      let big_64_02 = 0xFFFFFFFFFFFFFFFF+          patq_64_02 = "~fipfes-fipfes-fipfes-fipfes"+      Co.renderPatq (Co.patq big_64_02) `shouldBe` patq_64_02++    it "matches 128-bit reference values" $ do+      let big_128_01  = 0x00000000000000010000000000000000+          patq_128_01 = "~nec-dozzod-dozzod-dozzod-dozzod"+      Co.renderPatq (Co.patq big_128_01) `shouldBe` patq_128_01++      let big_128_02  = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF+          patq_128_02 =+            "~fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes-fipfes"++      Co.renderPatq (Co.patq big_128_02) `shouldBe` patq_128_02 
test/Ob.hs view
@@ -11,5 +11,5 @@   hspec $ do     context "small input space" S.tests     context "medium input space" M.tests-    context "propert tests" P.tests+    context "property tests" P.tests     context "unit tests" U.tests
+ test/Title.hs view
@@ -0,0 +1,10 @@+module Main where++import qualified Title.Tests.Property as P+import Test.Hspec++main :: IO ()+main =+  hspec $+    context "property tests" P.tests+
+ test/Title/Tests/Property.hs view
@@ -0,0 +1,45 @@++module Title.Tests.Property (+  tests+  ) where++import Data.Word (Word8, Word16, Word32, Word64)+import Test.Hspec+import Test.QuickCheck+import Urbit.Ob.Co (Patp)+import qualified Urbit.Ob.Co as Co+import qualified Urbit.Ob.Title as Title++galaxies :: Gen Patp+galaxies = fmap (Co.patp . fromIntegral) (arbitrary :: Gen Word8)++stars :: Gen Patp+stars = do+  star <- arbitrary `suchThat` (> (0xFF :: Word16))+  return (Co.patp (fromIntegral star))++planets :: Gen Patp+planets = do+  planet <- arbitrary `suchThat` (> (0xFFFF :: Word32))+  return (Co.patp (fromIntegral planet))++moons :: Gen Patp+moons = do+  moon <- arbitrary `suchThat` (> (0xFFFFFFFF :: Word64))+  return (Co.patp (fromIntegral moon))++tests :: Spec+tests =+  describe "clan" $ do+    it "identifies galaxies correctly" $+      forAll galaxies $ \x -> Title.clan x == Title.Galaxy++    it "identifies stars correctly" $+      forAll stars $ \x -> Title.clan x == Title.Star++    it "identifies planets correctly" $+      forAll planets $ \x -> Title.clan x == Title.Planet++    it "identifies moons correctly" $+      forAll moons $ \x -> Title.clan x == Title.Moon+
urbit-hob.cabal view
@@ -1,5 +1,5 @@ name:           urbit-hob-version:        0.2.0+version:        0.3.0 synopsis:       Hoon-style atom manipulation and printing functions homepage:       https://github.com/urbit/urbit-hob bug-reports:    https://github.com/urbit/urbit-hob/issues@@ -12,29 +12,43 @@ build-type:     Simple cabal-version:  >= 1.10 description:-  Here you can primarily find functions for dealing with the \"patp\" phonetic-  base used by Urbit.  The \@p encoding is used for naming ships; it uniquely-  represents a 32-bit number in a memorable and pronounceable fashion.+  Here you can primarily find functions for dealing with the \"patp\" and+  \"patq\" /phonetic bases/ used by Urbit.  The \@p encoding is used for naming+  ships, while the \@q encoding is used for arbitrary data; they each uniquely+  represent an underlying natural number (or /atom/) in a memorable and+  pronounceable fashion.   .-  The \@p encoding is an /obfuscated/ representation of an underlying 32-bit-  number, in particular, hence the \"ob\" in the library's name.+  The \@p encoding is an /obfuscated/ representation of an underlying atom, in+  particular, hence the \"ob\" in the library's name.   .-  The @Urbit.Ob@ module exposes two functions, 'patp' and 'fromPatp', for-  converting between representations.  You can also render a 'Patp' value via-  the 'render' function, or parse one via 'parse'.+  The @Urbit.Ob@ module exposes two families of functions, 'patp' and+  'fromPatp', and then 'patq' and 'fromPatq', for converting between+  representations.  You can also render a 'Patp' or 'Patq' value as 'Text' via+  the 'renderPatp' and 'renderPatq' functions, or parse them from 'Text' via+  'parsePatp' and 'parsePatq'.   .+  Since \@p values represent ships, some utilities for dealing with ships are+  also exposed.  The 'clan' and 'sein' functions are useful for determining a+  ship's class and (default) parent, respectively.+  .   Some quick examples:   .   >>> :set -XOverloadedStrings   >>> import qualified Urbit.Ob as Ob-  >>>   >>> let nidsut = Ob.patp 15663360-  >>> Ob.render nidsut+  >>> let marzod = Ob.patq (Ob.fromPatp nidsut)+  >>> Ob.renderPatp nidsut   "~nidsut-tomdun"+  >>> Ob.renderPatq marzod+  "~mun-marzod"   >>> Ob.fromPatp nidsut   15663360-  >>> Ob.parse "~nidsut-tomdun"+  >>> Ob.parsePatp "~nidsut-tomdun"   Right ~nidsut-tomdun+  >>> Ob.clan nidsut+  Planet+  >>> Ob.sein nidsut+  ~marzod  source-repository head   type: git@@ -50,7 +64,7 @@   hs-source-dirs:   lib    if flag(release)-    ghc-options:     -Wall+    ghc-options:     -Wall -O2   else     ghc-options:     -Wall -Werror @@ -59,6 +73,7 @@     , Urbit.Ob.Co     , Urbit.Ob.Muk     , Urbit.Ob.Ob+    , Urbit.Ob.Title    other-modules:       Data.Serialize.Extended@@ -105,5 +120,33 @@     , hspec-core     , QuickCheck     , text+    , urbit-hob++Test-suite title+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Title.hs+  other-modules:+    Title.Tests.Property+  default-language:    Haskell2010+  ghc-options:+    -rtsopts+  build-depends:+      base+    , hspec+    , QuickCheck+    , urbit-hob++benchmark ob-bench+  type:                exitcode-stdio-1.0+  hs-source-dirs:      bench+  main-is:             Ob.hs+  default-language:    Haskell2010+  ghc-options:+    -rtsopts -O2+  build-depends:+      base+    , criterion+    , deepseq     , urbit-hob