{-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE PatternSynonyms #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- ghc 7.10 module BNFC.Utils ( ModuleName , when, unless, unlessNull, unlessNull' , applyWhen, applyUnless , for, whenJust , caseMaybe, (>.>) , curry3, uncurry3 , singleton, headWithDefault, mapHead, spanEnd , duplicatesOn, groupOn, uniqOn , hasNumericSuffix , (+++), (++++), (+-+), (+.+), parensIf , pad, table , mkName, mkNames, NameStyle(..) , lowerCase, upperCase, mixedCase , camelCase, camelCase_ , snakeCase, snakeCase_ , replace , writeFileRep , cstring , getZonedTimeTruncatedToSeconds , symbolToName ) where import Control.Arrow ((&&&)) import Control.DeepSeq (rnf) import Data.Char import Data.List (intercalate) import Data.List.NonEmpty (pattern (:|)) import Data.Map (Map) #if !MIN_VERSION_base(4,11,0) import Data.Semigroup (Semigroup(..)) #endif import Data.Time import qualified Data.Foldable as Fold import qualified Data.Map as Map import qualified Data.List.NonEmpty as List1 import System.IO (IOMode(ReadMode),hClose,hGetContents,openFile) import System.IO.Error (tryIOError) import BNFC.PrettyPrint (Doc, text) type List1 = List1.NonEmpty -- | The name of a module, e.g. "Foo.Abs", "Foo.Print" etc. type ModuleName = String -- * Control flow. -- ghc 7.10 misses the instance Monoid a => Monoid (IO a) #if __GLASGOW_HASKELL__ <= 710 instance {-# OVERLAPPING #-} Semigroup (IO ()) where (<>) = (>>) instance {-# OVERLAPPING #-} Monoid (IO ()) where mempty = return () mappend = (<>) mconcat = sequence_ #endif -- | Generalization of 'Control.Monad.when'. when :: Monoid m => Bool -> m -> m when True m = m when False _ = mempty -- | Generalization of 'Control.Monad.unless'. unless :: Monoid m => Bool -> m -> m unless False m = m unless True _ = mempty -- | 'when' for the monoid of endofunctions 'a -> a'. applyWhen :: Bool -> (a -> a) -> a -> a applyWhen True f = f applyWhen False _ = id -- | 'unless' for the monoid of endofunctions 'a -> a'. applyUnless :: Bool -> (a -> a) -> a -> a applyUnless False f = f applyUnless True _ = id -- | Invoke continuation for non-empty list. unlessNull :: Monoid m => [a] -> ([a] -> m) -> m unlessNull l k = case l of [] -> mempty as -> k as -- | Invoke continuation for non-empty list. unlessNull' :: Monoid m => [a] -> (a -> [a] -> m) -> m unlessNull' l k = case l of [] -> mempty (a:as) -> k a as -- * Flipped versions of standard functions. infixr 8 >.> -- | Diagrammatic composition. (>.>) :: (a -> b) -> (b -> c) -> a -> c g >.> f = f . g -- | Non-monadic 'forM'. for :: [a] -> (a -> b) -> [b] for = flip map -- | Generalization of 'forM' to 'Monoid'. whenJust :: Monoid m => Maybe a -> (a -> m) -> m whenJust = flip foldMap -- | Rotation of 'maybe'. caseMaybe :: Maybe a -> b -> (a -> b) -> b caseMaybe ma b f = maybe b f ma -- * Tuple utilities. -- From https://hackage.haskell.org/package/extra-1.6.18/docs/Data-Tuple-Extra.html -- | Converts an uncurried function to a curried function. curry3 :: ((a, b, c) -> d) -> a -> b -> c -> d curry3 f a b c = f (a,b,c) -- | Converts a curried function to a function on a triple. uncurry3 :: (a -> b -> c -> d) -> ((a, b, c) -> d) uncurry3 f ~(a,b,c) = f a b c -- * String operations for printing. infixr 5 +++, ++++, +-+, +.+ -- | Concatenate strings by a space. (+++) :: String -> String -> String a +++ b = a ++ " " ++ b -- | Concatenate strings by a newline. (++++) :: String -> String -> String a ++++ b = a ++ "\n" ++ b -- | Concatenate strings by an underscore. (+-+) :: String -> String -> String a +-+ b = a ++ "_" ++ b -- | Concatenate strings by a dot. (+.+) :: String -> String -> String a +.+ b = a ++ "." ++ b -- | Wrap in parentheses if condition holds. parensIf :: Bool -> String -> String parensIf = \case True -> ("(" ++) . (++ ")") False -> id -- | Pad a string on the right by spaces to reach the desired length. pad :: Int -> String -> String pad n s = s ++ drop (length s) (replicate n ' ') -- | Make a list of rows with left-aligned columns from a matrix. table :: String -> [[String]] -> [String] table sep m = map (intercalate sep . zipWith pad widths) m where -- Column widths. widths :: [Int] widths = columns maximum $ map (map length) m -- Aggregate columns (works even for a ragged matrix with rows of different length). columns :: ([a] -> b) -> [[a]] -> [b] columns f rows = -- Take the values of the first column case concat (map (take 1) rows) of -- Matrix was empty: [] -> [] -- Matrix was non-empty: col -> f col : columns f (map (drop 1) rows) -- * List utilities -- | Give a telling name to the electric monkey. singleton :: a -> [a] singleton = (:[]) -- | Get the first element of a list, fallback for empty list. headWithDefault :: a -> [a] -> a headWithDefault a [] = a headWithDefault _ (a:_) = a -- | Apply a function to the head of a list. mapHead :: (a -> a) -> [a] -> [a] mapHead f = \case [] -> [] a:as -> f a : as -- | @spanEnd p l == reverse (span p (reverse l))@. -- -- Invariant: @l == front ++ end where (end, front) = spanEnd p l@ -- -- (From package ghc, module Util.) spanEnd :: (a -> Bool) -> [a] -> ([a], [a]) spanEnd p l = go l [] [] l where go yes _ rev_no [] = (yes, reverse rev_no) go yes rev_yes rev_no (x:xs) | p x = go yes (x : rev_yes) rev_no xs | otherwise = go xs [] (x : rev_yes ++ rev_no) xs -- | Replace all occurences of a value by another value replace :: Eq a => a -- ^ Value to replace -> a -- ^ Value to replace it with -> [a] -> [a] replace x y xs = [ if z == x then y else z | z <- xs] -- | Returns lists of elements whose normal form appears more than once. -- -- >>> duplicatesOn id [5,1,2,5,1] -- [1 :| [1],5 :| [5]] -- >>> duplicatesOn abs [5,-5,1] -- [5 :| [-5]] duplicatesOn :: (Foldable t, Ord b) => (a -> b) -> t a -> [List1 a] duplicatesOn nf -- Keep groups of size >= 2. = filter ((2 <=) . List1.length) -- Turn into a list of listss: elements grouped by their normal form. . Map.elems -- Partition elements by their normal form. . Fold.foldr (\ a -> Map.insertWith (<>) (nf a) (a :| [])) Map.empty -- | Group consecutive elements that have the same normalform. groupOn :: Eq b => (a -> b) -> [a] -> [List1 a] groupOn nf = loop where loop = \case [] -> [] a:xs | let (as, rest) = span ((nf a ==) . nf) xs -> (a :| as) : loop rest -- | Keep only the first of consecutive elements that have the same normalform. uniqOn :: Eq b => (a -> b) -> [a] -> [a] uniqOn nf = map List1.head . groupOn nf -- | Get a numeric suffix if it exists. -- -- >>> hasNumericSuffix "hello world" -- Nothing -- >>> hasNumericSuffix "a1b2" -- Just ("a1b",2) -- >>> hasNumericSuffix "1234" -- Just ("",1234) hasNumericSuffix :: String -> Maybe (String, Integer) hasNumericSuffix s = case spanEnd isDigit s of ([], _) -> Nothing (num, front) -> Just (front, read num) -- * Time utilities -- | Cut away fractions of a second in time. truncateZonedTimeToSeconds :: ZonedTime -> ZonedTime truncateZonedTimeToSeconds (ZonedTime (LocalTime day (TimeOfDay h m s)) zone) = ZonedTime (LocalTime day (TimeOfDay h m $ fromIntegral sec)) zone where sec :: Int sec = truncate s getZonedTimeTruncatedToSeconds :: IO ZonedTime getZonedTimeTruncatedToSeconds = truncateZonedTimeToSeconds <$> getZonedTime -- * File utilities -- | Write a file, after making a backup of an existing file with the same name. -- If an old version of the file exist and the new version is the same, -- keep the old file and don't create a .bak file. -- / New version by TH, 2010-09-23 writeFileRep :: FilePath -> String -> IO () writeFileRep path s = either newFile updateFile =<< tryIOError (readFile' path) where -- Case: file does not exist yet. newFile _ = do putStrLn $ "writing new file " ++ path writeFile path s -- Case: file exists with content @old@. updateFile old = do -- Write new content. writeFile path s if s == old -- test is O(1) space, O(n) time then do putStrLn $ "refreshing unchanged file " ++ path else do let bak = path ++ ".bak" putStrLn $ "writing file " ++ path ++ " (saving old file as " ++ bak ++ ")" writeFile bak old -- Force reading of contents of files to achieve compatibility with -- Windows IO handling, as combining lazy IO with `readFile` and -- 2x `renameFile` on the open `path` file complains with: -- -- "bnfc.exe: Makefile: MoveFileEx "Makefile" "Makefile.bak": permission -- denied (The process cannot access the file because it is being used -- by another process.)" readFile' :: FilePath -> IO String readFile' path' = do inFile <- openFile path' ReadMode contents <- hGetContents inFile rnf contents `seq` hClose inFile return contents -- *** Naming *** -- Because naming is hard (http://blog.codinghorror.com/i-shall-call-it-somethingmanager/) -- | Different case style data NameStyle = LowerCase -- ^ e.g. @lowercase@ | UpperCase -- ^ e.g. @UPPERCASE@ | SnakeCase -- ^ e.g. @snake_case@ | CamelCase -- ^ e.g. @CamelCase@ | MixedCase -- ^ e.g. @mixedCase@ | OrigCase -- ^ Keep original capitalization and form. deriving (Show, Eq) -- | Generate a name in the given case style taking into account the reserved -- word of the language. Note that despite the fact that those name are mainly -- to be used in code rendering (type Doc), we return a String here to allow -- further manipulation of the name (like disambiguation) which is not possible -- in the Doc type. -- -- Examples: -- -- >>> mkName [] LowerCase "FooBAR" -- "foobar" -- -- >>> mkName [] UpperCase "FooBAR" -- "FOOBAR" -- -- >>> mkName [] SnakeCase "FooBAR" -- "foo_bar" -- -- >>> mkName [] CamelCase "FooBAR" -- "FooBAR" -- -- >>> mkName [] CamelCase "Foo_bar" -- "FooBar" -- -- >>> mkName [] MixedCase "FooBAR" -- "fooBAR" -- -- >>> mkName ["foobar"] LowerCase "FooBAR" -- "foobar_" -- -- >>> mkName ["foobar", "foobar_"] LowerCase "FooBAR" -- "foobar__" mkName :: [String] -> NameStyle -> String -> String mkName reserved style s = notReserved name' where notReserved name | name `elem` reserved = notReserved (name ++ "_") | otherwise = name tokens = parseIdent s name' = case style of LowerCase -> map toLower $ concat tokens UpperCase -> map toUpper $ concat tokens CamelCase -> concatMap capitalize tokens MixedCase -> mapHead toLower $ concatMap capitalize tokens SnakeCase -> map toLower $ intercalate "_" tokens OrigCase -> s capitalize = mapHead toUpper -- | Same as above but accept a list as argument and make sure that the -- names generated are uniques. -- -- >>> mkNames ["c"] LowerCase ["A", "b_", "a_", "c"] -- ["a1","b","a2","c_"] mkNames :: [String] -> NameStyle -> [String] -> [String] mkNames reserved style = disambiguateNames . map (mkName reserved style) -- | This one takes a list of names and makes sure each is unique, appending -- numerical suffix if needed. -- -- >>> disambiguateNames ["a", "b", "a", "c"] -- ["a1","b","a2","c"] disambiguateNames :: [String] -> [String] disambiguateNames = disamb [] where disamb ns1 (n:ns2) | n `elem` (ns1 ++ ns2) = let i = length (filter (==n) ns1) + 1 in (n ++ show i) : disamb (n:ns1) ns2 | otherwise = n : disamb (n:ns1) ns2 disamb _ [] = [] -- | Heuristic to "parse" an identifier into separate components. -- -- >>> parseIdent "abc" -- ["abc"] -- -- >>> parseIdent "Abc" -- ["Abc"] -- -- >>> parseIdent "WhySoSerious" -- ["Why","So","Serious"] -- -- >>> parseIdent "why_so_serious" -- ["why","so","serious"] -- -- >>> parseIdent "why-so-serious" -- ["why","so","serious"] -- -- Some corner cases: -- -- >>> parseIdent "LBNFParser" -- ["LBNF","Parser"] -- -- >>> parseIdent "aLBNFParser" -- ["a","LBNF","Parser"] -- -- >>> parseIdent "ILoveNY" -- ["I","Love","NY"] parseIdent :: String -> [String] parseIdent = p [] . map (classify &&& id) where classify :: Char -> CharClass classify c | isUpper c = U | isLower c = L | otherwise = O p :: String -> [(CharClass,Char)] -> [String] -- Done: p acc [] = emit acc [] -- Continue if consecutive characters have same case. p acc ((L,c) : cs@((L,_) : _)) = p (c:acc) cs p acc ((U,c) : cs@((U,_) : _)) = p (c:acc) cs -- Break if consecutive characters have different case. p acc ((U,c) : cs@((L,_) : _)) = emit acc $ p [c] cs p acc ((L,c) : cs@((U,_) : _)) = emit (c:acc) $ p [] cs -- Discard "other" characters, and break to next component. p acc ((U,c) : (O,_) : cs) = emit (c:acc) $ p [] cs p acc ((L,c) : (O,_) : cs) = emit (c:acc) $ p [] cs p acc ((O,_) : cs) = emit acc $ p [] cs p acc [(_,c)] = p (c:acc) [] emit :: String -> [String] -> [String] emit [] rest = rest emit acc rest = reverse acc : rest data CharClass = U | L | O -- | Ident to lower case. -- >>> lowerCase "MyIdent" -- myident lowerCase :: String -> Doc lowerCase = text . mkName [] LowerCase -- | Ident to upper case. -- >>> upperCase "MyIdent" -- MYIDENT upperCase :: String -> Doc upperCase = text . mkName [] UpperCase -- | Ident to camel case. -- >>> camelCase "my_ident" -- MyIdent camelCase :: String -> Doc camelCase = text . camelCase_ camelCase_ :: String -> String camelCase_ = mkName [] CamelCase -- | To mixed case. -- >>> mixedCase "my_ident" -- myIdent mixedCase :: String -> Doc mixedCase = text . mkName [] MixedCase -- | To snake case. -- >>> snakeCase "MyIdent" -- my_ident snakeCase :: String -> Doc snakeCase = text . snakeCase_ snakeCase_ :: String -> String snakeCase_ = mkName [] SnakeCase -- ESCAPING -- | A function that renders a c-like string with escaped characters. -- Note that although it's called cstring, this can be used with most (all) -- backend as they seem to mostly share escaping conventions. -- The c in the name is barely an homage for C being the oldest language in -- the lot. -- -- >>> cstring "foobar" -- "foobar" -- -- >>> cstring "foobar\"" -- "foobar\"" cstring :: String -> Doc cstring = text . show -- * Symbols -- | Print a symbol as typical token name, like "(" as "LPAREN". symbolToName :: String -> Maybe String symbolToName = (`Map.lookup` symbolTokenMap) -- | Map from symbol to token name. symbolTokenMap :: Map String String symbolTokenMap = Map.fromList symbolTokenList -- | Association list from symbol to token name. symbolTokenList :: [(String, String)] symbolTokenList = [ ("{" , "LBRACE") , ("}" , "RBRACE") , ("(" , "LPAREN") , (")" , "RPAREN") , ("[" , "LBRACK") , ("]" , "RBRACK") , ("[]" , "EMPTYBRACK") , ("." , "DOT") , (".." , "DDOT") , ("...", "ELLIPSIS") , ("," , "COMMA") , (",," , "DCOMMA") , (";" , "SEMI") , (";;" , "DSEMI") , (":" , "COLON") , ("::" , "DCOLON") , (":=" , "COLONEQ") , ("::=", "DCOLONEQ") , (":-" , "COLONMINUS") , ("::-", "DCOLONMINUS") , (":--", "COLONDMINUS") , ("+" , "PLUS") , ("++" , "DPLUS") , ("+=" , "PLUSEQ") , ("+-" , "PLUSMINUS") , ("-" , "MINUS") , ("--" , "DMINUS") , ("-=" , "MINUSEQ") , ("-+" , "MINUSPLUS") , ("-*" , "MINUSSTAR") , ("*" , "STAR") , ("**" , "DSTAR") , ("*=" , "STAREQ") , ("*-" , "STARMINUS") , ("/" , "SLASH") , ("//" , "DSLASH") , ("/=" , "SLASHEQ") , ("\\" , "BACKSLASH") , ("\\\\","DBACKSLASH") , ("/\\", "WEDGE") , ("\\/", "VEE") , ("&" , "AMP") , ("&&" , "DAMP") , ("&=" , "AMPEQ") , ("|" , "BAR") , ("||" , "DBAR") , ("|=" , "BAREQ") , ("<" , "LT") , ("<<" , "DLT") , ("<<<", "TLT") , ("<=" , "LTEQ") , ("<<=", "DLTEQ") , ("<<<=","TLTEQ") , (">" , "GT") , (">>" , "DGT") , (">>>", "TGT") , (">=" , "GTEQ") , (">>=", "DGTEQ") , (">>>=","TGTEQ") , ("<>" , "LTGT") , ("=" , "EQ") , ("==" , "DEQ") , ("_" , "UNDERSCORE") , ("!" , "BANG") , ("!=" , "BANGEQ") , ("?" , "QUESTION") , ("?=" , "QUESTIONEQ") , ("#" , "HASH") , ("##" , "DHASH") , ("###", "THASH") , ("@" , "AT") , ("@@" , "DAT") , ("@=" , "ATEQ") , ("$" , "DOLLAR") , ("$$" , "DDOLLAR") , ("%" , "PERCENT") , ("%%" , "DPERCENT") , ("%=" , "PERCENTEQ") , ("^" , "CARET") , ("^^" , "DCARET") , ("^=" , "CARETEQ") , ("~" , "TILDE") , ("~~" , "DTILDE") , ("~=" , "TILDEEQ") , ("'" , "APOSTROPHE") , ("''" , "DAPOSTROPHE") , ("'''", "TAPOSTROPHE") , ("<-" , "LARROW") , ("->" , "RARROW") , ("<=" , "LDARROW") , ("=>" , "RDARROW") , ("|->", "MAPSTO") ]