|
1 |
| -{-# LANGUAGE TemplateHaskell #-} |
| 1 | +{-# LANGUAGE CPP #-} |
| 2 | +{-# LANGUAGE OverloadedStrings #-} |
| 3 | +{-# LANGUAGE TemplateHaskell #-} |
2 | 4 |
|
3 |
| -{-| This module provides `staticDhallExpression` which can be used to resolve |
4 |
| - all of an expression’s imports at compile time, allowing one to reference |
5 |
| - Dhall expressions from Haskell without having a runtime dependency on the |
6 |
| - location of Dhall files. |
| 5 | +-- | Template Haskell utilities |
| 6 | +module Dhall.TH |
| 7 | + ( -- * Template Haskell |
| 8 | + staticDhallExpression |
| 9 | + , makeHaskellTypeFromUnion |
| 10 | + ) where |
| 11 | + |
| 12 | +import Data.Monoid ((<>)) |
| 13 | +import Data.Text (Text) |
| 14 | +import Data.Text.Prettyprint.Doc (Pretty) |
| 15 | +import Dhall.Syntax (Expr(..)) |
| 16 | +import Language.Haskell.TH.Quote (dataToExpQ) -- 7.10 compatibility. |
| 17 | + |
| 18 | +import Language.Haskell.TH.Syntax |
| 19 | + ( Con(..) |
| 20 | + , Dec(..) |
| 21 | + , Exp(..) |
| 22 | + , Q |
| 23 | + , Type(..) |
| 24 | +#if MIN_VERSION_template_haskell(2,11,0) |
| 25 | + , Bang(..) |
| 26 | + , SourceStrictness(..) |
| 27 | + , SourceUnpackedness(..) |
| 28 | +#else |
| 29 | + , Strict(..) |
| 30 | +#endif |
| 31 | + ) |
| 32 | + |
| 33 | +import qualified Data.Text as Text |
| 34 | +import qualified Data.Text.Prettyprint.Doc.Render.String as Pretty |
| 35 | +import qualified Data.Typeable as Typeable |
| 36 | +import qualified Dhall |
| 37 | +import qualified Dhall.Map |
| 38 | +import qualified Dhall.Pretty |
| 39 | +import qualified Dhall.Util |
| 40 | +import qualified GHC.IO.Encoding |
| 41 | +import qualified Numeric.Natural |
| 42 | +import qualified System.IO |
| 43 | +import qualified Language.Haskell.TH.Syntax as Syntax |
| 44 | + |
| 45 | +{-| This fully resolves, type checks, and normalizes the expression, so the |
| 46 | + resulting AST is self-contained. |
| 47 | +
|
| 48 | + This can be used to resolve all of an expression’s imports at compile time, |
| 49 | + allowing one to reference Dhall expressions from Haskell without having a |
| 50 | + runtime dependency on the location of Dhall files. |
7 | 51 |
|
8 | 52 | For example, given a file @".\/Some\/Type.dhall"@ containing
|
9 | 53 |
|
|
22 | 66 | at compile time with all imports resolved, making it easy to keep your Dhall
|
23 | 67 | configs and Haskell interpreters in sync.
|
24 | 68 | -}
|
25 |
| -module Dhall.TH |
26 |
| - ( -- * Template Haskell |
27 |
| - staticDhallExpression |
28 |
| - ) where |
29 |
| - |
30 |
| -import Data.Typeable |
31 |
| -import Language.Haskell.TH.Quote (dataToExpQ) -- 7.10 compatibility. |
32 |
| -import Language.Haskell.TH.Syntax |
| 69 | +staticDhallExpression :: Text -> Q Exp |
| 70 | +staticDhallExpression text = do |
| 71 | + Syntax.runIO (GHC.IO.Encoding.setLocaleEncoding System.IO.utf8) |
33 | 72 |
|
34 |
| -import qualified Data.Text as Text |
35 |
| -import qualified Dhall |
36 |
| -import qualified GHC.IO.Encoding |
37 |
| -import qualified System.IO |
| 73 | + expression <- Syntax.runIO (Dhall.inputExpr text) |
38 | 74 |
|
39 |
| --- | This fully resolves, type checks, and normalizes the expression, so the |
40 |
| --- resulting AST is self-contained. |
41 |
| -staticDhallExpression :: Text.Text -> Q Exp |
42 |
| -staticDhallExpression text = do |
43 |
| - runIO (GHC.IO.Encoding.setLocaleEncoding System.IO.utf8) |
44 |
| - expression <- runIO (Dhall.inputExpr text) |
45 |
| - dataToExpQ (\a -> liftText <$> cast a) expression |
| 75 | + dataToExpQ (\a -> liftText <$> Typeable.cast a) expression |
46 | 76 | where
|
47 | 77 | -- A workaround for a problem in TemplateHaskell (see
|
48 | 78 | -- https://stackoverflow.com/questions/38143464/cant-find-inerface-file-declaration-for-variable)
|
49 |
| - liftText = fmap (AppE (VarE 'Text.pack)) . lift . Text.unpack |
| 79 | + liftText = fmap (AppE (VarE 'Text.pack)) . Syntax.lift . Text.unpack |
| 80 | + |
| 81 | +{-| Convert a Dhall type to a Haskell type that does not require any new |
| 82 | + data declarations |
| 83 | +-} |
| 84 | +toSimpleHaskellType :: Pretty a => Expr s a -> Q Type |
| 85 | +toSimpleHaskellType dhallType = |
| 86 | + case dhallType of |
| 87 | + Bool -> do |
| 88 | + return (ConT ''Bool) |
| 89 | + |
| 90 | + Double -> do |
| 91 | + return (ConT ''Double) |
| 92 | + |
| 93 | + Integer -> do |
| 94 | + return (ConT ''Integer) |
| 95 | + |
| 96 | + Natural -> do |
| 97 | + return (ConT ''Numeric.Natural.Natural) |
| 98 | + |
| 99 | + Text -> do |
| 100 | + return (ConT ''Text) |
| 101 | + |
| 102 | + App List dhallElementType -> do |
| 103 | + haskellElementType <- toSimpleHaskellType dhallElementType |
| 104 | + |
| 105 | + return (AppT (ConT ''[]) haskellElementType) |
| 106 | + |
| 107 | + App Optional dhallElementType -> do |
| 108 | + haskellElementType <- toSimpleHaskellType dhallElementType |
| 109 | + |
| 110 | + return (AppT (ConT ''Maybe) haskellElementType) |
| 111 | + |
| 112 | + _ -> do |
| 113 | + let document = |
| 114 | + mconcat |
| 115 | + [ "Unsupported simple type\n" |
| 116 | + , " \n" |
| 117 | + , "Explanation: Not all Dhall alternative types can be converted to Haskell \n" |
| 118 | + , "constructor types. Specifically, only the following simple Dhall types are \n" |
| 119 | + , "supported as an alternative type or a field of an alternative type: \n" |
| 120 | + , " \n" |
| 121 | + , "• ❰Bool❱ \n" |
| 122 | + , "• ❰Double❱ \n" |
| 123 | + , "• ❰Integer❱ \n" |
| 124 | + , "• ❰Natural❱ \n" |
| 125 | + , "• ❰Text❱ \n" |
| 126 | + , "• ❰List a❱ (where ❰a❱ is also a simple type) \n" |
| 127 | + , "• ❰Optional a❱ (where ❰a❱ is also a simple type) \n" |
| 128 | + , " \n" |
| 129 | + , "The Haskell datatype generation logic encountered the following complex \n" |
| 130 | + , "Dhall type: \n" |
| 131 | + , " \n" |
| 132 | + , " " <> Dhall.Util.insert dhallType <> "\n" |
| 133 | + , " \n" |
| 134 | + , "... where a simpler type was expected." |
| 135 | + ] |
| 136 | + |
| 137 | + let message = Pretty.renderString (Dhall.Pretty.layout document) |
| 138 | + |
| 139 | + fail message |
| 140 | + |
| 141 | +-- | Convert a Dhall type to the corresponding Haskell constructor type |
| 142 | +toConstructor :: Pretty a => (Text, Maybe (Expr s a)) -> Q Con |
| 143 | +toConstructor (constructorName, maybeAlternativeType) = do |
| 144 | + let name = Syntax.mkName (Text.unpack constructorName) |
| 145 | + |
| 146 | +#if MIN_VERSION_template_haskell(2,11,0) |
| 147 | + let bang = Bang NoSourceUnpackedness NoSourceStrictness |
| 148 | +#else |
| 149 | + let bang = NotStrict |
| 150 | +#endif |
| 151 | + |
| 152 | + case maybeAlternativeType of |
| 153 | + Just (Record kts) -> do |
| 154 | + let process (key, dhallFieldType) = do |
| 155 | + haskellFieldType <- toSimpleHaskellType dhallFieldType |
| 156 | + |
| 157 | + return (Syntax.mkName (Text.unpack key), bang, haskellFieldType) |
| 158 | + |
| 159 | + varBangTypes <- traverse process (Dhall.Map.toList kts) |
| 160 | + |
| 161 | + return (RecC name varBangTypes) |
| 162 | + |
| 163 | + Just dhallAlternativeType -> do |
| 164 | + haskellAlternativeType <- toSimpleHaskellType dhallAlternativeType |
| 165 | + |
| 166 | + return (NormalC name [ (bang, haskellAlternativeType) ]) |
| 167 | + |
| 168 | + Nothing -> do |
| 169 | + return (NormalC name []) |
| 170 | + |
| 171 | +-- | Generate a Haskell datatype declaration from a Dhall union type where |
| 172 | +-- each union alternative corresponds to a Haskell constructor |
| 173 | +-- |
| 174 | +-- This comes in handy if you need to keep a Dhall type and Haskell type in |
| 175 | +-- sync. You make the Dhall type the source of truth and use Template Haskell |
| 176 | +-- to generate the matching Haskell type declaration from the Dhall type. |
| 177 | +-- |
| 178 | +-- For example, this Template Haskell splice: |
| 179 | +-- |
| 180 | +-- > Dhall.TH.makeHaskellTypeFromUnion "T" "< A : { x : Bool } | B >" |
| 181 | +-- |
| 182 | +-- ... generates this Haskell code: |
| 183 | +-- |
| 184 | +-- > data T = A {x :: GHC.Types.Bool} | B |
| 185 | +-- |
| 186 | +-- If you are starting from an existing record type that you want to convert to |
| 187 | +-- a Haskell type, wrap the record type in a union with one alternative, like |
| 188 | +-- this: |
| 189 | +-- |
| 190 | +-- > Dhall.TH.makeHaskellTypeFromUnion "T" "< A : ./recordType.dhall >" |
| 191 | +-- |
| 192 | +-- To add any desired instances (such as `Dhall.FromDhall`/`Dhall.ToDhall`), |
| 193 | +-- you can use the `StandaloneDeriving` language extension, like this: |
| 194 | +-- |
| 195 | +-- > {-# LANGUAGE DeriveAnyClass #-} |
| 196 | +-- > {-# LANGUAGE DeriveGeneric #-} |
| 197 | +-- > {-# LANGUAGE OverloadedStrings #-} |
| 198 | +-- > {-# LANGUAGE StandaloneDeriving #-} |
| 199 | +-- > {-# LANGUAGE TemplateHaskell #-} |
| 200 | +-- > |
| 201 | +-- > Dhall.TH.makeHaskellTypeFromUnion "T" "< A : { x : Bool } | B >" |
| 202 | +-- > |
| 203 | +-- > deriving instance Generic T |
| 204 | +-- > deriving instance FromDhall T |
| 205 | +makeHaskellTypeFromUnion |
| 206 | + :: Text |
| 207 | + -- ^ Name of the generated Haskell type |
| 208 | + -> Text |
| 209 | + -- ^ Dhall code that evaluates to a union type |
| 210 | + -> Q [Dec] |
| 211 | +makeHaskellTypeFromUnion typeName text = do |
| 212 | + Syntax.runIO (GHC.IO.Encoding.setLocaleEncoding System.IO.utf8) |
| 213 | + |
| 214 | + expression <- Syntax.runIO (Dhall.inputExpr text) |
| 215 | + |
| 216 | + case expression of |
| 217 | + Union kts -> do |
| 218 | + let name = Syntax.mkName (Text.unpack typeName) |
| 219 | + |
| 220 | + constructors <- traverse toConstructor (Dhall.Map.toList kts ) |
| 221 | + |
| 222 | + let declaration = DataD [] name [] |
| 223 | +#if MIN_VERSION_template_haskell(2,11,0) |
| 224 | + Nothing |
| 225 | +#else |
| 226 | +#endif |
| 227 | + constructors [] |
| 228 | + |
| 229 | + return [ declaration ] |
| 230 | + |
| 231 | + _ -> do |
| 232 | + let document = |
| 233 | + mconcat |
| 234 | + [ "Dhall.TH.makeHaskellTypeFromUnion: Unsupported Dhall type\n" |
| 235 | + , " \n" |
| 236 | + , "Explanation: This function only coverts Dhall union types to Haskell datatype \n" |
| 237 | + , "declarations. \n" |
| 238 | + , " \n" |
| 239 | + , "For example, this is a valid Dhall union type that this function would accept: \n" |
| 240 | + , " \n" |
| 241 | + , " \n" |
| 242 | + , " ┌──────────────────────────────────────────────────────────────────┐ \n" |
| 243 | + , " │ Dhall.TH.makeHaskellTypeFromUnion \"T\" \"< A : { x : Bool } | B >\" │ \n" |
| 244 | + , " └──────────────────────────────────────────────────────────────────┘ \n" |
| 245 | + , " \n" |
| 246 | + , " \n" |
| 247 | + , "... which corresponds to this Haskell type declaration: \n" |
| 248 | + , " \n" |
| 249 | + , " \n" |
| 250 | + , " ┌──────────────────────────────────────┐ \n" |
| 251 | + , " │ data T = A {x :: GHC.Types.Bool} | B │ \n" |
| 252 | + , " └──────────────────────────────────────┘ \n" |
| 253 | + , " \n" |
| 254 | + , " \n" |
| 255 | + , "... but the following Dhall type is rejected due to being a bare record type: \n" |
| 256 | + , " \n" |
| 257 | + , " \n" |
| 258 | + , " ┌──────────────────────────────────────────────────────┐ \n" |
| 259 | + , " │ Dhall.TH.makeHaskellTypeFromUnion \"T\" \"{ x : Bool }\" │ Not valid \n" |
| 260 | + , " └──────────────────────────────────────────────────────┘ \n" |
| 261 | + , " \n" |
| 262 | + , " \n" |
| 263 | + , "If you are starting from a file containing only a record type and you want to \n" |
| 264 | + , "generate a Haskell type from that, then wrap the record type in a union with one\n" |
| 265 | + , "alternative, like this: \n" |
| 266 | + , " \n" |
| 267 | + , " \n" |
| 268 | + , " ┌──────────────────────────────────────────────────────────────────┐ \n" |
| 269 | + , " │ Dhall.TH.makeHaskellTypeFromUnion \"T\" \"< A : ./recordType.dhall >\" │ \n" |
| 270 | + , " └──────────────────────────────────────────────────────────────────┘ \n" |
| 271 | + , " \n" |
| 272 | + , " \n" |
| 273 | + , "The Haskell datatype generation logic encountered the following Dhall type: \n" |
| 274 | + , " \n" |
| 275 | + , " " <> Dhall.Util.insert expression <> "\n" |
| 276 | + , " \n" |
| 277 | + , "... which is not a union type." |
| 278 | + ] |
| 279 | + |
| 280 | + let message = Pretty.renderString (Dhall.Pretty.layout document) |
| 281 | + |
| 282 | + fail message |
0 commit comments