about summary refs log tree commit diff
path: root/pkgs/profpatsch/xmonad/DhallTypedInput.hs
blob: 18c32b226da15a331ad5621a8a9c575e532d1a96 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
{-# language RecordWildCards, NamedFieldPuns, OverloadedStrings, ScopedTypeVariables, KindSignatures, DataKinds, ScopedTypeVariables, RankNTypes, GADTs, TypeApplications, AllowAmbiguousTypes, LambdaCase #-}
{- Exports the `inputWithTypeArgs` function, which is able to read dhall files of the normalized form

@
\(CustomType: Type) ->
\(AnotherType: Type) ->
@

and set their actual representation on the Haskell side:

This has various advantages:

- dhall files still type check & normalize with the normal dhall
  tooling, they are standalone (and the types can be instantiated from
  dhall as well without any workarounds)
- It can be used like the default `input` function, no injection of
  custom symbols in the Normalizer is reqired
- Brings this style of dhall integration to Haskell, where it was only
  feasible in nix before, because that is untyped

The dhall types can be instantiated by every Haskell type that has an
`Interpret` instance. The “name” of the type lambda variable is
compared on the Haskell side with a type-level string that the user
provides, to prevent mixups.

TODO:
- Improve error messages (!)
- Provide a way to re-use the type mapping on the Haskell side, so
  that the returned values are not just the normal `Interpret` types,
  but the mapped ones (with name phantom type)
-}
module DhallTypedInput
( inputWithTypeArgs, TypeArg(..), TypeArgError(..), TypeArgEx(..),  typeArg
)
where

import Control.Monad.Trans.State.Strict as State
import Data.List (foldl')
import Control.Exception (Exception)
import qualified Control.Exception
import qualified Data.Text as Text

import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import Data.Proxy (Proxy(Proxy))

import Dhall (Type(..), InvalidType(..), InputSettings(..), EvaluateSettings(..), rootDirectory, startingContext, normalizer, standardVersion, sourceName, defaultEvaluateSettings, Interpret(..), auto)
import Dhall.TypeCheck (X)
import Dhall.Core
import Dhall.Parser (Src(..))
import qualified Dhall.Import
import qualified Dhall.Pretty
import qualified Dhall.TypeCheck
import qualified Dhall.Parser

import Lens.Family (LensLike', set, view)

import Data.Text.Prettyprint.Doc (Pretty)
import qualified Data.Text.Prettyprint.Doc               as Pretty
import qualified Data.Text.Prettyprint.Doc.Render.Text   as Pretty
import qualified Data.Text.Prettyprint.Doc.Render.String as Pretty


-- | Information about a type argument in the dhall input
--
-- If the dhall file starts with @\(CustomType : Type) ->@,
-- that translates to @TypeArg "CustomType" interpretionType@
-- where @"CustomType"@ is a type-level string describing the
-- name of the type in the dhall file (as a sanity check) and
-- @interpretationType@ is any type which implements
-- 'Dhall.Interpret'.
--
-- This is basically a specialized 'Data.Proxy'.
data TypeArg (sym :: Symbol) t = TypeArg

-- | Existential wrapper of a 'TypeArg', allows to create a list
-- of heterogenous 'TypeArg's.
data TypeArgEx
  where TypeArgEx :: (KnownSymbol sym, Interpret t) => TypeArg sym t -> TypeArgEx

-- | Shortcut for creating a 'TypeArgEx'.
--
-- Use with @TypeApplications@:
--
-- @
-- typeArg @"CustomType" @Integer
-- @
typeArg :: forall sym t. (KnownSymbol sym, Interpret t) => TypeArgEx
typeArg = TypeArgEx (TypeArg :: TypeArg sym t)

-- | Possible errors returned when applying a 'TypeArg'
-- to a 'Dhall.Expr'.
data TypeArgError
  = WrongLabel Text.Text
  -- ^ The name (label) of the type was different,
  -- the text value is the expected label.
  | NoLambda
  -- ^ The 'Dhall.Expr' does not start with 'Dhall.Lam'.

-- | Apply a 'TypeArg' to a 'Dhall.Expr'.
--
-- Checks that the dhall file starts with the 'Dhall.Lam'
-- corresponding to 'TypeArg`, then applies @t@ (dhall type application)
-- and normalizes, effectively stripping the 'Dhall.Lam'.
applyTypeArg
  :: forall sym t. (KnownSymbol sym, Interpret t)
  => Expr Src X
  -> TypeArg sym t
  -> Either TypeArgError (Expr Src X)
applyTypeArg expr ta@(TypeArg) = case expr of
  (Lam label (Const Dhall.Core.Type) _)
    -> let expectedLabel = getLabel ta
        in if label /= getLabel ta
           then Left (WrongLabel expectedLabel)
           else let expr' = (normalize (App expr tExpect))
                  in Right expr'
    where
        Dhall.Type _ tExpect = Dhall.auto :: Dhall.Type t
  expr -> Left NoLambda

-- | Inflect the type-level string @sym@ to a text value.
getLabel :: forall sym t. (KnownSymbol sym) => TypeArg sym t -> Text.Text
getLabel _ = Text.pack $ symbolVal (Proxy :: (Proxy :: Symbol -> *) sym)

instance (KnownSymbol sym) => Show (TypeArg sym t) where
  show TypeArg =
    "TypeArg "
    ++ (symbolVal (Proxy :: (Proxy :: Symbol -> *) sym))

-- | Takes a list of 'TypeArg's and parses the given
-- dhall string, applying the given 'TypeArg's in order
-- to the opaque dhall type arguments (see 'TypeArg' for
-- how these should look).
--
-- This is a slightly changed 'Dhall.inputWith'.
--
-- Discussion: Any trace of our custom type is removed from
-- the resulting
inputWithTypeArgs
  :: InputSettings
  -> [TypeArgEx]
  -> Dhall.Type a
  -> Text.Text
  -> IO a
inputWithTypeArgs settings typeArgs (Dhall.Type {extract, expected}) txt = do
    expr <- throws (Dhall.Parser.exprFromText (view sourceName settings) txt)

    -- TODO: evaluateSettings not exposed
    -- let evSettings = view evaluateSettings settings
    let evSettings :: EvaluateSettings = defaultEvaluateSettings

    -- -vvv copied verbatim from 'Dhall.inputWith' vvv-
    let transform =
               set Dhall.Import.standardVersion
               (view standardVersion evSettings)
            .  set Dhall.Import.normalizer
               (view normalizer evSettings)
            .  set Dhall.Import.startingContext
               (view startingContext evSettings)

    let status = transform (Dhall.Import.emptyStatus
                            (view rootDirectory settings))

    expr' <- State.evalStateT (Dhall.Import.loadWith expr) status
    -- -^^^ copied verbatim ^^^-

    let
      -- | if there’s a note, run the transformation and rewrap with the note
      skipNote e f = case e of
          Note n e -> Note n $ f e
          e -> f e

    let
      -- | strip one 'TypeArg'
      stripTypeArg :: Expr Src X -> TypeArgEx -> Expr Src X
      stripTypeArg e (TypeArgEx ta) = skipNote e $ \e' -> case e' of
          (Lam label _ _) ->
            case applyTypeArg e' ta of
              Right e'' -> e''
              -- TODO obvously improve error messages
              Left (WrongLabel l) ->
                error $ "Wrong label, should have been `" ++ Text.unpack l ++ "` but was `" ++ Text.unpack label ++ "`"
              Left NoLambda -> error $ "I expected a lambda of the form λ(" ++ Text.unpack label ++ ": Type) → but got: " ++ show e
          e' -> error $ show e'

    -- strip all 'TypeArg's
    let expr'' = foldl' stripTypeArg expr' typeArgs

    -- -vvv copied verbatim as well (expr' -> expr'') vvv-
    let suffix = prettyToStrictText expected
    let annot = case expr'' of
            Note (Src begin end bytes) _ ->
                Note (Src begin end bytes') (Annot expr'' expected)
              where
                bytes' = bytes <> " : " <> suffix
            _ ->
                Annot expr'' expected

    _ <- throws (Dhall.TypeCheck.typeWith (view startingContext settings) annot)
    case extract (Dhall.Core.normalizeWith (Dhall.Core.getReifiedNormalizer (view normalizer settings)) expr'') of
        Just x  -> return x
        Nothing -> Control.Exception.throwIO InvalidType


-- copied from Dhall.Pretty.Internal
prettyToStrictText :: Pretty a => a -> Text.Text
prettyToStrictText = docToStrictText . Pretty.pretty

-- copied from Dhall.Pretty.Internal
docToStrictText :: Pretty.Doc ann -> Text.Text
docToStrictText = Pretty.renderStrict . Pretty.layoutPretty options
  where
   options = Pretty.LayoutOptions { Pretty.layoutPageWidth = Pretty.Unbounded }

-- copied from somewhere in Dhall
throws :: Exception e => Either e a -> IO a
throws (Left  e) = Control.Exception.throwIO e
throws (Right r) = return r


-- TODO: add errors like these
-- data WrongTypeLabel = WrongTypeLabel deriving (Typeable)

-- _ERROR :: String
-- _ERROR = "\ESC[1;31mError\ESC[0m"

-- instance Show WrongTypeLabel where
--     show WrongTypeLabel =
--         _ERROR <> ": Mislabelled type lambda
--         \                                                                                \n\
--         \Expected your t provide an extract function that succeeds if an expression      \n\
--         \matches the expected type.  You provided a Type that disobeys this contract     \n"