1-- | Parser combinators for the POSIX @Makefile@ specifications.2module Mach.Parser where34import Control.Exception (throwIO)5import Control.Monad (unless, void, when)6import Data.List (elemIndices)7import Mach.Error (MakeErr (..))8import qualified Mach.Types as T9import Mach.Util (isSpecial)10import System.IO (hGetContents, stdin)11import Text.ParserCombinators.Parsec12 ( Parser,13 alphaNum,14 anyChar,15 between,16 char,17 eof,18 lookAhead,19 many,20 many1,21 manyTill,22 newline,23 noneOf,24 oneOf,25 optionMaybe,26 parse,27 parseFromFile,28 sepBy,29 sepBy1,30 skipMany,31 string,32 try,33 unexpected,34 (<|>),35 )3637-- Bind a given character to the given result.38bind :: String -> a -> Parser a39bind str val = val <$ string str4041-- | Parse one or more newlines.42newlines :: Parser ()43newlines = void $ many1 newline4445-- | Parse a single blank character.46blank :: Parser Char47blank = char ' '4849-- | Parse one or more <blank> characters.50blanks :: Parser ()51blanks = void $ many1 blank5253-- Parse zero or more <blank> characters.54maybeBlanks :: Parser ()55maybeBlanks = void $ many (char ' ')5657-- | Parse a character from the portable filename character set.58--59-- See https://pubs.opengroup.org/onlinepubs/007904975/basedefs/xbd_chap03.html#tag_03_27660fnChar :: Parser Char61fnChar = alphaNum <|> oneOf "._-"6263-- | Parse a macro name, according to portable macro names should64-- consist exclusively of characters from the portabel filename65-- character set.66macroName :: Parser String67macroName = many1 fnChar6869-- | Parse a target name character. As per POSIX, target names should70-- only consist of slashes, hyphens, periods, underscores, digits and71-- alphabetics.72targetChar :: Parser Char73targetChar = alphaNum <|> oneOf "/-._"7475-- | Parse an assignment operator, also refered to as a macro flavor76-- in the POSIX standard. The implementation provided here should77-- be aligned with the 'Show' instance of 'T.Flavor'.78assignOp :: Parser T.Flavor79assignOp =80 bind "=" T.Delayed81 <|> bind "::=" T.Immediate82 <|> bind ":::=" T.StrictDelay83 <|> bind "!=" T.System84 <|> bind "?=" T.Cond85 <|> bind "+=" T.Append8687-- | Parse an assignment, i.e. a macro definition.88assign :: Parser T.Assign89assign = do90 mident <- tokens' [' ', '=']91 flavor <- blanks >> assignOp <* blanks92 T.Assign mident flavor <$> tokens9394-- | Parse a macro expansion.95macroExpand :: Parser T.Token96macroExpand = char '$' >> (singleChar <|> macroExpand')97 where98 singleChar :: Parser T.Token99 singleChar = T.Exp <$> ((\x -> T.Lit [x]) <$> literal "{}()")100101-- | Parse a macro expansion enclosed in parentheses.102macroExpand' :: Parser T.Token103macroExpand' =104 let inner = try subExpand <|> simpleExpand105 in ( between (char '(') (char ')') inner106 <|> between (char '{') (char '}') inner107 )108 where109 -- Parse a macro expansion of the form $(string1).110 simpleExpand :: Parser T.Token111 simpleExpand =112 T.Exp113 <$> ( macroExpand114 <|> macroExpand115 <|> tokenLit (literal "})")116 )117118 -- Parse a macro expansion of the form $(string1:subst1=[subst2]).119 subExpand :: Parser T.Token120 subExpand = do121 -- TODO: Support nested macro expansion in string1122 string1 <- T.Exp <$> (tokenLit $ literal ":})")123 _ <- char ':'124 subst1 <- many1 $ noneOf "="125 _ <- char '='126 subst2 <- many $ noneOf "})"127 pure $ T.ExpSub string1 subst1 subst2128129-- | Parse a valid character for a literal.130-- Characters not valid in the current context can be passed as well.131literal :: [Char] -> Parser Char132literal notValid = noneOf $ notValid ++ "#\n\\$"133134-- | Parse a single token, i.e. an escaped newline, escaped @$@ character, macro expansion, or literal.135token :: Parser T.Token136token = tokenLit $ literal []137138-- | Parse a token but use a custom parser for parsing of literal tokens.139tokenLit :: Parser Char -> Parser T.Token140tokenLit lit =141 try macroExpand142 <|> escDollar143 <|> escNewline144 <|> litToken145 where146 escDollar :: Parser T.Token147 escDollar = bind "$$" (T.Lit "$")148149 escNewline :: Parser T.Token150 escNewline = T.Lit " " <$ (string "\\\n" >> maybeBlanks)151152 -- TODO: In noneOf, check that \ is followed by a newline.153 litToken :: Parser T.Token154 litToken = T.Lit <$> many1 lit155156-- | Parse a sequence of zero or more 'T.Token'.157tokens :: Parser T.Token158tokens = T.Seq <$> many token159160-- | Like 'tokens' but allows passing characters not valid in the current context.161tokens' :: [Char] -> Parser T.Token162tokens' notValid = T.Seq <$> many (tokenLit $ literal notValid)163164-- | Parse multiple lines of commands prefixed by a tab character.165commands :: Parser [T.Token]166commands = many (char '\t' >> (tokens <* newline))167168-- | Inference rule.169infRule :: Parser T.InfRule170infRule = do171 target <- char '.' >> (:) '.' <$> many1 targetChar172173 let periods = length $ elemIndices '.' target174 when (isSpecial target || (periods /= 1 && periods /= 2)) $175 unexpected "invalid inference rule target name"176177 cmds <-178 char ':'179 >> ( (const [] <$> (many1 blank >> char ';'))180 <|> (many blank >> newline >> commands)181 )182183 pure $ T.InfRule target cmds184185-- | Target rule which defines how targets are build.186targetRule :: Parser T.TgtRule187targetRule = do188 targets <- sepBy1 (tokenLit targetChar) blank189 _ <- char ':' >> (blank <|> lookAhead newline)190 prereqs <- sepBy (tokenLit targetChar) blank191 command <- optionMaybe (char ';' >> tokens)192 _ <- newline193194 cmds <- commands195 pure $ T.TgtRule targets prereqs (maybe cmds (: cmds) command)196197include :: Parser [T.Token]198include = do199 _ <- optionMaybe (char '-')200 _ <- string "include" >> blanks201 paths <- sepBy1 (tokenLit $ literal " ") blank202 _ <- maybeBlanks203 pure paths204205skipNoCode :: Parser ()206skipNoCode = do207 _ <- many blank208 _ <- many newline209 _ <- skipMany (try comment >> many blank >> many newline)210 pure ()211 where212 comment :: Parser String213 comment = char '#' >> manyTill anyChar newline214215lexeme :: Parser a -> Parser a216lexeme p = p <* skipNoCode217218-- | Parse a POSIX @Makefile@.219mkFile :: Parser T.MkFile220mkFile =221 skipNoCode222 >> many223 ( try (T.MkAssign <$> lexeme assign)224 <|> try (T.MkInfRule <$> lexeme infRule)225 <|> try (T.MkTgtRule <$> lexeme targetRule)226 <|> try (T.MkInclude <$> lexeme include)227 )228 -- Ensure that we parse the whole Makefile229 <* lexeme eof230231------------------------------------------------------------------------232233-- | Parse assignments and targets specified on the command-line.234cmdLine :: String -> IO (T.MkFile, [FilePath])235cmdLine str =236 case parse cmdLine' "command-line" str of237 Left err -> throwIO $ ParserErr err238 Right mk -> pure mk239 where240 assign' :: Parser T.Assign241 assign' = do242 mident <- tokens' [' ', '=']243 flavor <- assignOp244 unless (flavor `elem` [T.Delayed, T.Immediate, T.StrictDelay]) $245 unexpected "invalid assignment flavor"246247 T.Assign mident flavor <$> tokens' " "248249 cmdLine' :: Parser (T.MkFile, [FilePath])250 cmdLine' = do251 assigns <- many (try assign' <* many blank)252 targets <- sepBy (many1 targetChar) blank253 pure (map T.MkAssign assigns, targets)254255parseMkFile :: FilePath -> IO T.MkFile256parseMkFile path = do257 res <-258 if path == "-"259 then parse mkFile path <$> hGetContents stdin260 else parseFromFile mkFile path261 case res of262 Left err -> throwIO $ ParserErr err263 Right mk -> pure mk