fortran-src-0.4.2: src/Language/Fortran/Rewriter/Internal.hs
{-# LANGUAGE OverloadedStrings #-}
-- Original code from Bloomberg, used with permission.
--
-- Original authors:
-- * Daniel Beer
-- * Anthony Burzillo
-- * Raoul Hidalgo Charman
-- * Aiden Jeffrey
-- * Jason Xu
-- * Beleth Apophis
-- * Lukasz Kolodziejczyk
module Language.Fortran.Rewriter.Internal where
import Data.Int
import Data.ByteString.Lazy.Char8 ( ByteString )
import qualified Data.ByteString.Lazy.Char8 as BC
import Control.Exception ( Exception
, throw
)
import Data.List ( sort
, find
)
import Data.Maybe ( isNothing
, fromMaybe
, fromJust
, maybeToList
)
import qualified Data.Map as M
import Data.Typeable ( Typeable )
-- | Represents location in source code.
--
-- Note that, 'SourceLocation' indicates space between characters,
-- i.e the following example:
--
-- @ SourceLocation 0 1 @
--
-- indicates position between first and second characters in a file.
data SourceLocation = SourceLocation Int Int deriving (Show, Eq)
-- | Represents range in source code.
data SourceRange = SourceRange SourceLocation SourceLocation deriving (Eq)
instance Show SourceRange where
show (SourceRange (SourceLocation l1 c1) (SourceLocation l2 c2)) =
"("
++ show (l1 + 1) ++ ":" ++ show (c1 + 1)
++ ")-("
++ show (l2 + 1) ++ ":" ++ show (c2 + 1)
++ ")"
-- | Represents a character in the original source text along with
-- any replacement operations applied to the character in place.
--
-- It expects a character (in case it's empty, Nothing should be used),
-- whether it should be removed, its 'SourceLocation' and a string that
-- should be put in place of it.
data RChar = RChar (Maybe Char) Bool SourceLocation ByteString deriving (Show, Eq)
-- | Represents the intent to replace content in the file.
--
-- The content in 'Replacement' will be used in place of what is in
-- the range described. Note that the replacement text can be shorter
-- or larger than the original span, and it can also be multi-line.
data Replacement = Replacement SourceRange String deriving (Show, Eq)
instance Ord Replacement where
(Replacement (SourceRange a _) _) <= (Replacement (SourceRange b _) _) =
a < b
-- | Exception raised when two 'Replacement' objects overlap
-- ('OverlappingError') or 'Replacement' points at invalid locations
-- ('InvalidRangeError').
data ReplacementError
= OverlappingError [(Replacement, Replacement)]
| InvalidRangeError
deriving (Show, Typeable, Eq)
-- | As we advance through the ['RChar'] list, we consider "chunks"
-- as the unit of text written out. A chunk is either:
--
-- 1. original source text up to a newline character, end of file
-- or 'RChar' described in 2.
-- 2. a single 'RChar' that has non-empty replacement string
-- or is deleted.
type Chunk = [RChar]
-- | Represents map of files and replacements that will be done.
type ReplacementMap = M.Map String [Replacement]
instance Exception ReplacementError
instance Ord SourceLocation where
(SourceLocation l1 c1) <= (SourceLocation l2 c2) =
l1 < l2 || l1 == l2 && c1 <= c2
-- | Parses input string into a list of annotated characters.
toRCharList :: ByteString -> [RChar]
toRCharList = reverse . uncurry appendLast . BC.foldl'
go
(SourceLocation 0 0, [])
where
go :: (SourceLocation, [RChar]) -> Char -> (SourceLocation, [RChar])
go (loc@(SourceLocation line col), rcs) c =
let newLoc = if c /= '\n'
then SourceLocation line (col + 1)
else SourceLocation (line + 1) 0
in (newLoc, RChar (Just c) False loc "" : rcs)
appendLast loc = (RChar Nothing False loc "" :)
-- | Marks 'RChars' in a given range to be removed later.
markRChars :: [RChar] -> SourceRange -> [RChar]
markRChars rchars sr = markRChars_ rchars sr (SourceLocation 0 0)
markRChars_ :: [RChar] -> SourceRange -> SourceLocation -> [RChar]
markRChars_ [] _ _ = []
markRChars_ (RChar x odel _ orepl : xs) sr@(SourceRange (SourceLocation sl sc) (SourceLocation el ec)) (SourceLocation l c) =
rch : rchs
where
rch =
if l == sl && l == el && c >= sc && c < ec
|| l == sl && l < el && c >= sc
|| l == el && l > sl && c < ec
|| l > sl && l < el
then RChar x True (SourceLocation l c) ""
else RChar x odel (SourceLocation l c) orepl
rchs =
if x /= Just '\n'
then markRChars_ xs sr (SourceLocation l (c + 1))
else markRChars_ xs sr (SourceLocation (l + 1) 0)
-- | Sets replacement string to be prepended to the given location.
setReplacementStringSL
:: [RChar] -> SourceLocation -> ByteString -> Bool -> [RChar]
setReplacementStringSL [] _ _ _ = []
setReplacementStringSL (RChar och odel osl@(SourceLocation ol oc) orepl : xs) sl@(SourceLocation l c) repl isInsert
= if l == ol && c == oc
then if isInsert
then
RChar och
odel
osl
-- (repl <> if isNothing och then "" else [fromJust och])
(repl <> maybe "" BC.singleton och)
: xs
else RChar och odel osl repl : xs
else RChar och odel osl orepl : setReplacementStringSL xs sl repl isInsert
-- | Sets replacement string to be prepended to the begining of the
-- given range.
setReplacementStringSR
:: [RChar] -> SourceRange -> ByteString -> Bool -> [RChar]
setReplacementStringSR rchars (SourceRange sls _) =
setReplacementStringSL rchars sls
-- | Applies all deletions and additions and transforms 'RChars' back
-- to a string.
evaluateRChars :: [RChar] -> ByteString
evaluateRChars = BC.concat . map evaluateRChar
-- | If 'RChar' is marked as deleted, it'll be evaluated to its
-- replacement string, otherwise original character will be returned.
evaluateRChar :: RChar -> ByteString
evaluateRChar (RChar char del _ repl) | del = repl
| isNothing char = ""
| otherwise = BC.singleton $ fromJust char
-- | From ['RChar'], obtain a ('Chunk', ['RChars']) where the 'Chunk'
-- is the next 'Chunk' and the ['RChar'] are the remaining 'RChar's.
nextChunk :: [RChar] -> (Chunk, [RChar])
nextChunk [] = ([], [])
-- if the current chunk is the start of inline comment, prepend it to next
nextChunk (rchar@(RChar (Just '!') True _ _) : xs) = (rchar : fst rec, snd rec)
where rec = nextChunk xs
nextChunk (rchar@(RChar _ True _ _) : xs) = ([rchar], xs)
nextChunk rchars = nextChunk_ rchars
nextChunk_ :: [RChar] -> (Chunk, [RChar])
nextChunk_ [] = ([], [])
nextChunk_ ls@(RChar _ True _ _ : _) = ([], ls)
nextChunk_ (rchar@(RChar (Just '\n') _ _ _) : xs) = ([rchar], xs)
nextChunk_ (rchar : xs) = (rchar : fst rec, snd rec) where rec = nextChunk_ xs
-- | Splits ['RChar'] into 'Chunk's.
allChunks :: [RChar] -> [Chunk]
allChunks [] = []
allChunks rchars = chunk : allChunks rest
where (chunk, rest) = nextChunk rchars
-- | Transform a list of 'Chunk's into a single string, applying
-- continuation lines when neccessary.
evaluateChunks :: [Chunk] -> ByteString
evaluateChunks ls = evaluateChunks_ ls 0
evaluateChunks_ :: [Chunk] -> Int64 -> ByteString
evaluateChunks_ [] _ = ""
evaluateChunks_ (x : xs) currLen =
if overLength
then "\n +"
<> evaluateRChars xPadded
<> maybe (evaluateChunks_ xs (6 + nextLen)) (evaluateChunks_ xs)
lastLen
else chStr
<> maybe (evaluateChunks_ xs (currLen + nextLen)) (evaluateChunks_ xs)
lastLen
where
overLength = currLen + nextLen > 72 && currLen > 0
xPadded = padImplicitComments x (72 - 6)
chStr = evaluateRChars x
nextLen = fromMaybe
(BC.length chStr)
(myMin (BC.elemIndex '\n' chStr) (BC.elemIndex '!' chStr)) -- don't line break for comments
lastLen = BC.elemIndex '\n' $ BC.reverse chStr
-- min for maybes that doesn't short circuit if there's a Nothing
myMin y z = case (y, z) of
(Just a , Just b ) -> Just $ min a b
(Nothing, Just a ) -> Just a
(Just a , Nothing) -> Just a
(Nothing, Nothing) -> Nothing
-- Text after line 72 is an implicit comment, so should stay there
padImplicitComments :: Chunk -> Int -> Chunk
padImplicitComments chunk targetCol = case findCommentRChar chunk of
Just (index, rc) ->
take index chunk
++ padCommentRChar rc (targetCol - index + 1)
: drop (index + 1) chunk
Nothing -> chunk
where
findCommentRChar :: Chunk -> Maybe (Int, RChar)
findCommentRChar =
find ((\(RChar _ _ (SourceLocation _ cl) _) -> cl == 72) . snd)
. zip [1 ..]
padCommentRChar :: RChar -> Int -> RChar
padCommentRChar (RChar char _ loc repl) padding = RChar
char
True
loc
(BC.pack (replicate padding ' ' ++ maybeToList char) `BC.append` repl)
-- | Return TRUE iff the 'Replacement' constitutes a character
-- insertion.
isInsertion :: Replacement -> Bool
isInsertion (Replacement (SourceRange (SourceLocation sl sc) (SourceLocation el ec)) repl)
= sl == el && sc == ec && repl /= ""
insertionSR :: SourceRange -> SourceRange
insertionSR (SourceRange (SourceLocation sl sc) _) =
SourceRange (SourceLocation sl sc) (SourceLocation sl (sc + 1))
-- | Sets a single 'Replacement' given a list of 'RChar's.
setReplacement :: [RChar] -> Replacement -> [RChar]
setReplacement rchars repl@(Replacement sr replS) =
let replBS = BC.pack replS
in if isInsertion repl
then setReplacementStringSR (markRChars rchars (insertionSR sr))
(insertionSR sr)
replBS
True
else setReplacementStringSR (markRChars rchars sr) sr replBS False
-- | Sets a list of 'Replacement's given a list of 'RChar's.
setReplacements :: [RChar] -> [Replacement] -> [RChar]
setReplacements rchars repls =
let rchar' = foldl setReplacement rchars repls in adjustLineWrap rchar'
-- | heuristic to wrap line after comma or right parenthesis if applicable
adjustLineWrap :: [RChar] -> [RChar]
adjustLineWrap [] = []
adjustLineWrap [x] = [x]
adjustLineWrap (rc@(RChar _ True _ _) : rs@(RChar (Just c) False _ _ : _))
| c `elem` [',', ')'] = adjustLineWrapAux rc [] rs
adjustLineWrap (x : xs) = x : adjustLineWrap xs
adjustLineWrapAux :: RChar -> [RChar] -> [RChar] -> [RChar]
adjustLineWrapAux rc1 deleted (rc2@(RChar (Just c) False _ _) : rs)
| c `elem` [',', ')'] = adjustLineWrapAux (appendRC rc1 c)
(deleteRC rc2 : deleted)
rs
adjustLineWrapAux rc1 deleted rs = (rc1 : reverse deleted) <> adjustLineWrap rs
-- | Mark removal for the input 'RChar'
deleteRC :: RChar -> RChar
deleteRC (RChar _ _ loc s) = RChar Nothing True loc s
-- | Append the input character to the replacement string
appendRC :: RChar -> Char -> RChar
appendRC (RChar mc _ loc s) c = RChar mc True loc (s `BC.snoc` c)
-- | Checks whether two 'Replacement's are not overlapping.
areDisjoint :: Replacement -> Replacement -> Bool
areDisjoint (Replacement (SourceRange (SourceLocation r1sl r1sc) (SourceLocation r1el r1ec)) _) (Replacement (SourceRange (SourceLocation r2sl r2sc) (SourceLocation r2el r2ec)) _)
| r2sl > r1el || r1sl > r2el = True
| r1el == r2sl && r1ec <= r2sc = True
| r1sl == r2el && r1sc >= r2ec = True
| otherwise = False
-- | Checks whether:
--
-- 1. the start is before the end of the range and
-- 2. both start and end locations are within the code.
isValidRange :: SourceRange -> [RChar] -> Bool
isValidRange (SourceRange sl1 sl2) rchars =
sl1 <= sl2 && isValidLocation sl1 rchars && isValidLocation sl2 rchars
isValidLocation :: SourceLocation -> [RChar] -> Bool
isValidLocation _ [] = False
isValidLocation sl (RChar _ _ csl _ : xs) = sl == csl || isValidLocation sl xs
checkRanges :: [RChar] -> [Replacement] -> [RChar]
checkRanges rchars repls = if and validList
then rchars
else throw InvalidRangeError
where validList = [ isValidRange sr rchars | (Replacement sr _) <- repls ]
checkOverlapping :: [Replacement] -> [Replacement]
checkOverlapping repls = if null overlappingPairs
then repls
else throw $ OverlappingError overlappingPairs
where
overlappingPairs = findOverlappingPairs (sort repls)
findOverlappingPairs :: [Replacement] -> [(Replacement, Replacement)]
findOverlappingPairs [] = []
findOverlappingPairs repls' =
let currentRepl = head repls'
overlapping = takeWhile (not . areDisjoint currentRepl) (tail repls')
nextResult = findOverlappingPairs (tail repls')
in [ (currentRepl, x) | x <- overlapping ] <> nextResult
-- | Applies 'Replacement's to a string and return it.
--
-- Firstly, it transforms the string into a list of 'RChar's.
--
-- After that, it validates the 'SourceRange' of each 'Replacement'.
--
-- In the end, it splits up 'RChar's in 'Chunk's, set the
-- 'Replacement's and evaluates the 'Chunk's.
applyReplacements :: ByteString -> [Replacement] -> ByteString
applyReplacements str repls = applyReplacements_ (checkRanges rchars repls)
(checkOverlapping repls)
where rchars = toRCharList str
applyReplacements_ :: [RChar] -> [Replacement] -> ByteString
applyReplacements_ rchars repls = evaluateChunks chunks
where
replRchars = setReplacements rchars repls
chunks = allChunks replRchars