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 +89/−0
- lib/Urbit/Ob.hs +1/−0
- lib/Urbit/Ob/Co.hs +116/−27
- lib/Urbit/Ob/Title.hs +111/−0
- test/Co/Tests/Property.hs +31/−7
- test/Co/Tests/Unit.hs +42/−13
- test/Ob.hs +1/−1
- test/Title.hs +10/−0
- test/Title/Tests/Property.hs +45/−0
- urbit-hob.cabal +56/−13
+ 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