summaryrefslogtreecommitdiff
path: root/hadrian/src/Settings/Parser.hs
blob: d93f71ae06e3abfe5cfa0d4438147430ef1194cd (plain)
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
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
{-# LANGUAGE FlexibleContexts #-}
-- | Utilities for implementing key-value settings, as described in Note [Hadrian settings]
module Settings.Parser where

import Control.Applicative
import Control.Monad.Except
import Control.Monad.State as State
import Data.Either
import Data.List

import qualified Text.Parsec as Parsec

-- * Raw parsing of @key = value@ or @key += value@ expressions

-- | A 'Key' is parsed from a dot-separated list of words.
type Key = [String]

-- | A 'Val'ue is any 'String'.
type Val = String

-- | 'Equal' when overriding the entire computation of a setting with some
--   fresh values, 'PlusEqual' when extending it.
data Op = Equal | PlusEqual
  deriving (Eq, Ord, Show)

-- | A 'KeyVal' represents an expression @foo.bar.baz [+]= v@.
data KeyVal = KeyVal Key Op Val
  deriving (Eq, Ord, Show)

-- | Pretty-print 'KeyVal's.
ppKeyVals :: [KeyVal] -> String
ppKeyVals = unlines . map ppKeyVal

-- | Pretty-print a 'KeyVal'.
ppKeyVal :: KeyVal -> String
ppKeyVal (KeyVal k op v) =
  intercalate "." k ++ " " ++ opstr ++ " " ++ v

  where opstr = case op of
          Equal     ->  "="
          PlusEqual -> "+="

-- | Remove any string that can be parsed as a 'KeyVal' from the
--   given list.
removeKVs :: [String] -> [String]
removeKVs xs = fst (partitionKVs xs)

-- | Try to parse all strings of the given list as 'KeyVal's and keep
--   only the successful parses.
parseJustKVs :: [String] -> [KeyVal]
parseJustKVs xs = snd (partitionKVs xs)

-- | Try to parse all strings from the given list as 'KeyVal's and return
--   the ones for which parsing fails in the first component of the pair,
--   and the successful parses in the second component of the pair.
partitionKVs :: [String] -> ([String], [KeyVal])
partitionKVs xs = partitionEithers $
  map (\x -> either (const $ Left x) Right $ parseKV x) xs

-- | Try to parse all strings from the input list as 'KeyVal's.
parseKVs :: [String] -> [Either Parsec.ParseError KeyVal]
parseKVs = map parseKV

-- | Try to parse the given string as a 'KeyVal'.
parseKV :: String -> Either Parsec.ParseError KeyVal
parseKV = Parsec.parse parseKeyVal "<string list>"

-- | This implements a parser that supports @key = val@, @key = "val"@,
--   @key += val@, @key += "val"@ style syntax, where there can be 0 or more
--   spaces between the key and the operator, and the operator and the value.
parseKeyVal :: Parsec.Parsec String () KeyVal
parseKeyVal = do
  k <- parseKey
  skipSpaces
  op <- parseOp
  skipSpaces
  v <- parseValue
  return (KeyVal k op v)

  where skipSpaces = Parsec.optional (Parsec.many1 (Parsec.oneOf " \t"))

-- | Parse a dot-separated list of alpha-numerical words that can contain
--   dashes, just not at the beginning.
parseKey :: Parsec.Parsec String () Key
parseKey =
  Parsec.sepBy1 (starOr $ liftA2 (:) Parsec.alphaNum $
                  Parsec.many (Parsec.alphaNum <|> Parsec.char '-')
                )
                (Parsec.char '.')

  where starOr :: Parsec.Parsec String () String -> Parsec.Parsec String () String
        starOr p = ((\x -> [x]) <$> Parsec.char '*') <|> p

-- | Parse @=@ or @+=@.
parseOp :: Parsec.Parsec String () Op
parseOp = Parsec.choice
  [ Parsec.char '=' *> pure Equal
  , Parsec.string "+=" *> pure PlusEqual
  ]

-- | Parse @some string@ or @\"some string\"@.
parseValue :: Parsec.Parsec String () Val
parseValue = Parsec.optional (Parsec.char '\"') >> Parsec.manyTill Parsec.anyChar ((Parsec.char '\"' >> pure ()) <|> Parsec.eof)

-- * Expressing settings

-- | The current key component must match the given string.
str :: Match f => String -> f ()
str = matchString

-- | Like 'str', but returns the second argument insead of @()@.
--
-- > val s a = str s *> pure a
val :: Match f => String -> a -> f a
val s a = str s *> pure a

-- | Try and match one of the given "matchers".
--
-- > oneOf [str "hello", str "hi"] -- matches "hello" or "hi"
oneOf :: Match f => [f a] -> f a
oneOf = matchOneOf

-- | Try and match one of the given strings, returning the corresponding
--   value (the @a@) when the input matches.
choose :: Match f => [(String, a)] -> f a
choose xs = oneOf $ map (uncurry val) xs

-- | Try and match one of the given strings, or @*@, and return
--   the corresponding value (@One someValue@ or @Wildcard@ respectively).
wild :: Match f => [(String, a)] -> f (Wildcard a)
wild xs = choose $ ("*", Wildcard) : map (fmap One) xs

-- * Wildcards (@*@) in settings

-- | A @'Wildcard' a@ is either 'Wildcard' or @One x@ where @x :: a@.
data Wildcard a = Wildcard | One a
  deriving (Eq, Ord, Show)

-- | Elimination rule for 'Wildcard'. The first argument is returned
--   when the input is 'Wildcard', and when it's not the second argument
--   is applied to the value wrapped behind 'One'.
wildcard :: b -> (a -> b) -> Wildcard a -> b
wildcard z f x = case x of
  Wildcard -> z
  One a    -> f a

-- * 'Match' class, to interpret settings in various ways

-- 'matchOneOf' is similar in spirit to Alternative's '<|>',
-- but we don't really have an identity ('empty').
--
-- 'matchString' on the other hand is just a handy primitive.
--
-- Selective functors may be relevant here...?

-- | Equip the 'Applicative' class with a primitive to match a known string,
--   and another to try and match a bunch of "alternatives", returning
--   the first one that succeeds.
class Applicative f => Match f where
  matchString :: String -> f ()
  matchOneOf :: [f a] -> f a

-- * 'SettingsM' interpretation

type SettingError = String

type SettingsM = StateT Key (Either SettingError)

-- | Runs the 'SettingsM' computation, returning the value at the leaf
--   when the given 'Key' matches exactly at least one setting, erroring
--   out when it is too long or just doesn't match.
runSettingsM :: Key -> SettingsM a -> Either SettingError a
runSettingsM k m = case runStateT m k of
  Left  err     -> Left err
  Right (a, []) -> return a
  Right (_, xs) -> throwError $ "suffix " ++ show xs ++ " not found in settings"

-- | Stateful manipulation of the remaining key components,
--   with errors when strings don't match.
instance Match SettingsM where
  matchString = matchStringSettingsM
  matchOneOf = matchOneOfSettingsM

matchStringSettingsM :: String -> SettingsM ()
matchStringSettingsM s = do
  ks <- State.get
  if null ks
    then throwError $ "expected " ++ show s ++ ", got nothing"
    else go (head ks)

  where go k
          | k == s = State.modify tail
          | otherwise = throwError $
              "expected " ++ show s ++ ", got " ++ show k

matchOneOfSettingsM :: [SettingsM a] -> SettingsM a
matchOneOfSettingsM acts = StateT $ \k -> do
  firstMatch $ map (($ k) . State.runStateT) acts

  where firstMatch
          :: [Either SettingError (a, Key)]
          -> Either SettingError (a, Key)
        firstMatch []              = throwError "matchOneOf: no match"
        firstMatch (Left _ : xs)   = firstMatch xs
        firstMatch (Right res : _) = return res

-- * Completion interpretation

-- | A tree with values at the leaves ('Pure'), but where we can
--   have "linear links" with strings attached.
--
--   - @'Known' s t@ nodes are used to represent matching against
--     known strings;
--   - @'Branch' ts@ nodes are used to represent matching against a list
--     of "matchers";
--   - @'Pure' a@ nodes are used to attach values at the leaves, and help
--     provide an 'Applicative' interface.
data CompletionTree a
  = Known String (CompletionTree a)
  | Branch [CompletionTree a]
  | Pure a
  deriving (Eq, Show)

-- | Traverses 'Known' and 'Branch' nodes, only applying the
--   function to values at the leaves, wrapped with 'Pure'.
instance Functor CompletionTree where
  fmap f (Known s t) = Known s (fmap f t)
  fmap f (Branch ts) = Branch (map (fmap f) ts)
  fmap f (Pure a)    = Pure (f a)

-- | 'pure' is 'Pure', '<*>' distributes the choices.
instance Applicative CompletionTree where
  pure = Pure

  Pure f <*> Pure x    = Pure (f x)
  Pure f <*> Known s t = Known s (fmap f t)
  Pure f <*> Branch xs = Branch (map (fmap f) xs)
  Known s t <*> t'  = Known s (t <*> t')
  Branch ts <*> t'  = Branch (map (<*> t') ts)

-- | 'matchString' gets mapped to 'Known', 'matchOneOf' to 'Branch'.
instance Match CompletionTree where
  matchString s = Known s (Pure ())
  matchOneOf xs = Branch xs

-- | Enumerate all the keys a completion tree represents, with the corresponding
--   leave values.
--
--   > enumerate (val "hello" 1)) -- [(1, ["hello"])]
enumerate :: CompletionTree a -> [(a, Key)]
enumerate = go []

  where go ks (Known s t) = go (s:ks) t
        go ks (Branch xs) = concatMap (go ks) xs
        go ks (Pure a) = [(a, reverse ks)]

-- | Enumerate all the valid completions for the given input (a partially-written
--   setting key).
--
--   > complete ["hel"] (val 1 "hello")
--   >   -- returns [(1, ["hello"])]
--   > complete ["foo"] (str "foo" *> oneOf [val "hello" 1, val "world" 2])
--   >   -- returns [(1, ["foo", "hello"]), (2, ["foo", "world"])]
complete :: [String] -> CompletionTree a -> [(a, Key)]
complete [] t = enumerate t
complete (k:ks) t = case t of
  Known s t'
    | k == s -> map (fmap (s:)) (complete ks t')
    | (k `isPrefixOf` s) && null ks -> map (fmap (s:)) (enumerate t')
      -- TODO: use an Either to indicate suggestions about
      -- typos somewhere in the middle (not for the final component)
      -- (e.g "You wrote stage1.ghc-bi.ghc.hs.opts but probably
      -- meant stage1.ghc-bin.ghc.hs.opts") ?
    | otherwise -> []
  Branch ts -> concatMap (complete (k:ks)) ts
  Pure a -> return (a, [])