hat-2.7.0.0: tools/HatExplore.hs
-- module HatExplore where
import LowLevel (FileNode(..),openHatFile,nil,unevaluated,entered,interrupted
,NodeType(..),nodeType
,peekResult,peekSubExprs,peekExpArg,getParentNode
,hiddenChildren,getSrcRef,getDefnRef,getErrorLoc
,hatVersionNumber)
import qualified SrcRef (SrcRef(..))
import SrcRef (readSrcRef,defnSrcRef)
import Ident (Ident,getIdentAt)
import SExp (prettyEquation2)
import CommonUI (Options,initialOptions)
import Detect (findMain)
import CommonUI (safeReadInt,hatObserve,hatTrail,hatDetect,hatAnim)
import HighlightStyle
import System.IO (hSetBuffering,BufferMode(..),stdin,stdout,stderr,hPutStrLn)
import Foreign.C.String (withCString)
import Numeric (showHex)
import System.IO.Unsafe (unsafePerformIO)
import System.Cmd (system)
import System.Environment (getArgs,getProgName,getEnv)
import System.Exit (exitWith,ExitCode(..))
import Data.List (isSuffixOf,sortBy,partition,union)
import Control.Monad (liftM)
import Data.Char (isAlpha)
import Explore (Coord(..),noCoord,isNoCoord,isNextCoord,beforeCoord
,afterCoord,Row,Col
,Location(..),isVirtLocation,isNoLocation
,isImproperLocation,mkLocation,getLocation,hasUseLocation
,getDefLocation,getUseLocation
,redexParent)
main = do
hatFile <- initHatFile
startNode <- determineStartNode
startInteraction startNode
printNodes :: String -> [FileNode] -> IO ()
-- for testing only
printNodes str nodes =
putStrLn (str ++ ':' : foldr myShow "" (map int nodes))
where
myShow i xs = ',' : showHex i xs
showNode :: FileNode -> String
showNode = flip showHex "" . int
initHatFile :: IO FilePath
initHatFile = do
prog <- System.Environment.getProgName
hatFile <- determineHatFile
withCString prog (\p-> withCString hatFile (openHatFile p))
return hatFile
determineHatFile :: IO FilePath
determineHatFile = do
args <- System.Environment.getArgs
case args of (f:_) -> return (rectify f)
_ -> do hPutStrLn stderr
("error: no trace file")
exitWith (ExitFailure 1)
where
rectify :: FilePath -> FilePath
rectify f | ".hat" `isSuffixOf` f = f
| otherwise = f ++ ".hat"
dropHS :: FilePath -> FilePath
dropHS f = case reverse f of
's':'h':'.':name -> reverse name
's':'h':'l':'.':name -> reverse name
getErrorRedex :: IO FileNode
-- determine the redex that caused a runtime error
-- if no runtime error then return nil
getErrorRedex = do
errRedex <- getErrorLoc -- careful, might be nil, Hidden or trusted
return (firstWithSrcRef errRedex)
firstWithSrcRef :: FileNode -> FileNode
-- return redex or first ancestor in traced code that has source reference
-- redex may be nil, Hidden, or in trusted code (then has hidden parent)
firstWithSrcRef redex =
if redex /= nil && (getSrcRef redex == nil || isHidden (redexParent redex))
then firstWithSrcRef (redexParent redex)
else redex
determineStartNode :: IO FileNode
-- either Main.main or passed as first argument
determineStartNode = do
maybeNode <- getArgumentNode 1
case maybeNode of
Just node -> return node
Nothing -> do
errRedex <- getErrorRedex
if errRedex /= nil then
return errRedex
else do
mainNode <- findMain
if mainNode == LowLevel.nil then do
putStrLn "Could not find \"Main.main\"!"
exitWith (ExitFailure 1)
else
return mainNode
getArgumentNode :: Int -> IO (Maybe FileNode)
-- get node from nth program argument, if it exists
getArgumentNode argNo = do
arguments <- System.Environment.getArgs
return $ if length arguments <= argNo then
Nothing
else case safeReadInt 0 (arguments!!argNo) of
0 -> Nothing
i -> let node = (FileNode i)
in if expIsRedex node then Just node else
let parent = redexParent node
in if parent == nil then Nothing else Just parent
-- if given node is not a redex try its parent
--------------------------------------------------------------------
traverseReduct :: FileNode -> (Slice,[FileNode],[FileNode])
-- given node of redex determine
-- slice that covers part of reduct that *may* have been demanded (approxim.)
-- edt-children (demanded redexes (applications))
-- cafs (shared children)
traverseReduct redex =
if reduct `elem` [LowLevel.nil,unevaluated,entered,interrupted] then
(emptySlice,[],[])
else go reduct emptySlice
where
(parent,reduct) = case nodeType redex of
ExpConstUse ->
let constDef = peekExpArg redex 0
constRes = peekResult constDef
in if constRes `notElem` [nil,unevaluated,entered,interrupted]
then case nodeType constRes of
ExpProjection ->
let projParent = getParentNode constRes
in if projParent /= constDef
then (getParentNode projParent,projParent)
-- pattern binding
else (constDef,constRes)
_ -> (constDef,constRes)
else (constDef,constRes)
_ -> (redex,peekResult redex)
gos :: [FileNode] -> Slice -> (Slice,[FileNode],[FileNode])
gos [] slice = (slice,[],[])
gos (exp:exps) slice =
let (slice1,redexes1,consts1) = gos exps slice
(slice2,redexes2,consts2) = go exp slice1
in (slice2,redexes2++redexes1,consts2++consts1)
go :: FileNode -> Slice -> (Slice,[FileNode],[FileNode])
go exp slice = if getParentNode exp /= parent then (slice,[],[])
else case nodeType exp of
ExpApp ->
if isRedex exp then
let (newSlice,redexes,consts) =
gos (peekSubExprs exp) (addToSlice loc slice)
in (newSlice,exp:redexes,consts)
else (subtractFromSlice loc slice,[],[])
ExpValueApp ->
gos (tail (peekSubExprs exp)) (addToSlice loc slice)
-- first arg is atom for which there is no location
ExpValueUse -> (addToSlice loc slice,[],[])
ExpConstUse ->
if isRedex (peekExpArg exp 0) then -- check constDef
(addToSlice loc slice,[],[exp])
else
(subtractFromSlice loc slice,[],[])
ExpConstDef -> error "traverseReduct: ExpConstDef"
ExpGuard ->
let (slice1,redexes1,consts1) = traverseReduct exp
(slice2,redexes2,consts2) = go (peekExpArg exp 0) slice1
in (slice2,redexes1++redexes2,consts1++consts2)
-- sadly trace contains no useful location for guard
-- existing location is that of boolean expression
ExpCase ->
let (slice1,redexes1,consts1) = traverseReduct exp
slice2 = addToSlice (Location{begin=begin loc
,end=Coord{row=row (begin loc)
,col = col (begin loc)+3}})
(mergeSlices slice1 (subtractFromSlice loc slice))
(slice3,redexes2,consts2) = go (peekExpArg exp 0) slice2
in (slice3,redexes1++redexes2,consts1++consts2)
-- subtract whole subexpression from slice before adding "case"
-- and demanded parts
ExpIf ->
let (slice1,redexes1,consts1) = traverseReduct exp
slice2 = addToSlice (Location{begin=begin loc
,end=Coord{row=row (begin loc)
,col = col (begin loc)+1}})
(mergeSlices slice1 (subtractFromSlice loc slice))
(slice3,redexes2,consts2) = go (peekExpArg exp 0) slice2
in (slice3,redexes1++redexes2,consts1++consts2)
-- subtract whole subexpression from slice before adding "if"
-- and demanded parts
ExpFieldUpdate ->
if isRedex exp then
let (slice,redexes,consts) = gos (peekSubExprs exp)
(addToSlice loc slice)
in (slice,exp:redexes,consts)
else
(subtractFromSlice loc slice,[],[])
ExpProjection -> (addToSlice loc slice,[],[])
-- argument must have different parent
ExpHidden -> (emptySlice,hiddenChildren exp,[])
-- (slice,[],[])
ExpForward -> go (peekResult exp) slice -- just walk through
ExpChar -> (addToSlice loc slice,[],[])
ExpInt -> (addToSlice loc slice,[],[])
ExpInteger -> (addToSlice loc slice,[],[])
ExpRat -> (addToSlice loc slice,[],[])
ExpRational -> (addToSlice loc slice,[],[])
ExpFloat -> (addToSlice loc slice,[],[])
ExpDouble -> (addToSlice loc slice,[],[])
_ -> error "getReductSubExpNodes: unexpected node type"
where
(_,loc) = getUseLocation exp
isRedex :: FileNode -> Bool
-- assume there is a non-nil result
isRedex exp = peekResult exp `notElem` [unevaluated,exp]
-- partial application can have a result pointing to itself
--------------------------------------------------------------------
redexAllChildren :: FileNode -> [FileNode]
-- given a redex, i.e. application or constUse
-- yield all children, including trusted ones
-- children are applications or constUses
redexAllChildren redex = appRedexes ++ constRedexes
where
(_,appRedexes,constRedexes) = traverseReduct redex
expSort :: [FileNode] -> [FileNode]
expSort = sortBy cmp
where
cmp red1 red2 = compare loc1 loc2
where
(_,loc1) = getUseLocation red1
(_,loc2) = getUseLocation red2
redexChildren :: FileNode -> [FileNode]
-- excludes trusted children (currently even those with children, TO DO)
-- currently this function is not used at all
redexChildren = filter suspectedRedex . redexAllChildren
suspectedRedex :: FileNode -> Bool
-- given application or constUse
suspectedRedex redex =
result `elem` [entered,interrupted] ||
not (isHidden result && null (hiddenChildren result))
where
result = redexResult redex
redexResult :: FileNode -> FileNode
-- given application or constUse return the reduct (or entered, etc)
-- this is usually not the final result
redexResult redex = case nodeType redex of
ExpApp -> peekResult redex
ExpConstUse -> peekResult (peekExpArg redex 0)
ExpConstDef -> peekResult redex
-- the following are just for reliablity
ExpGuard -> peekResult redex
ExpCase -> peekResult redex
ExpIf -> peekResult redex
ExpFieldUpdate -> peekResult redex
_ -> error ("redexResult: unexpected redex " ++ showNode redex)
redexParents :: FileNode -> [FileNode]
-- list of ancestors, first is the parent
redexParents redex =
let parent = redexParent redex
in if parent == nil then [] else parent : redexParents parent
expIsRedex :: FileNode -> Bool
expIsRedex node = case nodeType node of
ExpApp -> isRedex node
ExpConstUse -> isRedex (peekExpArg node 0)
ExpConstDef -> isRedex node
_ -> False
isHidden :: FileNode -> Bool
isHidden node = case nodeType node of
ExpHidden -> True
_ -> False
--------------------------------------------------------------------
updRedexFaultyDescendants :: State -> State
-- all descendants including redex itself that may be faulty
-- i.e. descendant tree without subtrees started by a correct redex
updRedexFaultyDescendants state =
state{faultSetRoot = root
,faultSet = if root == nil then emptySet else go root id emptySet}
where
-- root is first wrong redex when going up parents
root = case filter (`elemSet` (wrong state))
(cur (redexes state):parents state) of
(wrong:_) -> wrong
[] -> nil
-- continuation passing style, threading already visited nodes
-- to avoid looping for mutually recursive constants
go :: FileNode -> (Set FileNode -> Set FileNode)
-> Set FileNode -> Set FileNode
go redex cont collected =
if redexCorrect redex state || redex `elemSet` collected
then cont collected
else (foldr ($) cont (map go (redexAllChildren redex)))
(insertSet redex collected)
--------------------------------------------------------------------
redexDefUsedSlice :: State -> FileNode -> Slice
redexDefUsedSlice state redex =
let (file,loc) = getDefLocation redex
in if file /= filename (cur (modules state))
then emptySlice
else used
where
(used,_,_) = traverseReduct redex
redexDefSlice :: State -> FileNode -> Slice
redexDefSlice state redex =
let (file,loc) = getDefLocation redex
in if file == filename (cur (modules state))
then mkSlice loc
else emptySlice
redexUseLocation :: FileNode -> Location
-- assumes given node is redex, i.e. application or constUse
-- location covers full redex, including all subexpressions
redexUseLocation redex =
if redex `elem` [nil,unevaluated,entered,interrupted] then
error "redexUseLocation: invalid redex"
else loc
where
(_,loc) = getUseLocation redex
redexUseSlice :: FileNode -> Slice
-- assumes given node is redex, i.e. application or constUse
-- slice includes the application and all subconstructs
-- that are values or may be values
redexUseSlice redex =
if redex `elem` [nil,unevaluated,entered,interrupted] then
error "redexUseSlice: invalid redex"
else gos (peekSubExprs redex) (mkSlice loc)
where
parent = getParentNode redex
(_,loc) = getUseLocation redex
gos :: [FileNode] -> Slice -> Slice
gos exps slice = foldr go slice exps
go :: FileNode -> Slice -> Slice
go exp slice =
if getParentNode exp /= parent then
slice
else case nodeType exp of
ExpApp -> subtractFromSlice loc slice
ExpValueApp -> gos (tail (peekSubExprs exp)) (addToSlice loc slice)
ExpValueUse -> addToSlice loc slice
ExpConstUse -> subtractFromSlice loc slice
ExpConstDef -> slice -- only if original redex was constUse
ExpGuard -> subtractFromSlice loc slice
ExpCase -> subtractFromSlice loc slice
ExpIf -> subtractFromSlice loc slice
ExpFieldUpdate -> subtractFromSlice loc slice
ExpChar -> addToSlice loc slice
ExpInt -> addToSlice loc slice
ExpInteger -> addToSlice loc slice
ExpRat -> addToSlice loc slice
ExpRational -> addToSlice loc slice
ExpFloat -> addToSlice loc slice
ExpDouble -> addToSlice loc slice
_ -> error "redexUseSlice: unexpected node type"
where
(_,loc) = getUseLocation exp
--------------------------------------------------------------------
borderSlice :: Text -> Slice
-- construct slice of all spaces at beginning and ends of lines
borderSlice text = Slice (go 1 text)
where
go :: Int -> Text -> [Location]
go _ [] = []
go curRow (line:rest) =
(if leftSpace > 0 then
(Location{begin=Coord{row=curRow,col=1}
,end=Coord{row=curRow,col=leftSpace}}:)
else id)
((Location{begin=Coord{row=curRow,col=lastCol-rightSpace+1}
,end=Coord{row=curRow,col=maxBound}}:)
(go (curRow+1) rest))
where
lastCol = length revLine
leftSpace = length (takeWhile (==' ') line)
rightSpace = length (takeWhile (==' ') revLine)
revLine = case reverse line of
'\r':rest -> rest
all -> all
--------------------------------------------------------------------
startInteraction :: FileNode -> IO ()
startInteraction startRedex = do
hSetBuffering stdin NoBuffering
hSetBuffering stdout NoBuffering
(width,height) <- getTerminalSize
putStr cls
let allParents = redexParents startRedex
current <- loadModule (fst (getLocation startRedex))
curModules <- insertModules allParents (mkCursorSeq [] current [])
checkModules (newRedex (State
{ modules = curModules
, window = Coord{row=height,col=width}
, firstRow = 1
, parents = allParents
, redexes = undefined
, correct = emptySet
, wrong = emptySet
, neither = emptySet
, faultSetRoot = nil
, faultSet = emptySet
, showFaultSet = False
, showPart = False
, message = "Press h for help."
})
startRedex)
fileTags :: State -> [Tag]
fileTags state =
let redexSlice = mkSlice (redexUseLocation (cur (redexes state)))
siblings = cursorSeqToList (redexes state)
(suspectedSiblings,trustedSiblings) = partition suspectedRedex siblings
(correctSus,otherSus) =
partition (`elemSet` (correct state)) suspectedSiblings
(wrongFaultySus,neitherSus) =
partition (`elemSet` (wrong state)) otherSus
(neitherTrust,otherTrust) =
partition (`elemSet` (neither state)) trustedSiblings
(wrongFaultyTrust,correctTrust) =
partition (`elemSet` (wrong state)) trustedSiblings
correctSiblings = correctTrust ++ correctSus
neitherSiblings = neitherTrust ++ neitherSus
(faultySiblings,wrongSiblings) =
partition (`redexFaulty` state) (wrongFaultyTrust ++ wrongFaultySus)
correctSiblingsSlice = mergeSlicesMap redexUseSlice correctSiblings
neitherSiblingsSlice = mergeSlicesMap redexUseSlice neitherSiblings
wrongSiblingsSlice = mergeSlicesMap redexUseSlice wrongSiblings
faultySiblingsSlice = mergeSlicesMap redexUseSlice faultySiblings
makeSlice = if showPart state then redexDefUsedSlice else redexDefSlice
in if filename (cur (modules state)) ==
fst (getUseLocation (cur (redexes state)))
then combineSlices
[(redexSlice,currentM)
,(correctSiblingsSlice,correctM)
,(wrongSiblingsSlice,wrongM)
,(neitherSiblingsSlice,markedM)
,(faultySiblingsSlice,faultyM)
,(border (cur (modules state)),emptyM)
,(if showFaultSet state
then manySlices (makeSlice state) (faultSet state)
else makeSlice state (cur (redexes state))
,definitionM)]
else case getLocation (cur (redexes state)) of
(file,loc) ->
combineSlices
((if file == filename (cur (modules state))
then ((mkSlice loc,currentM):)
else id)
[(border (cur (modules state)),emptyM)
,(if showFaultSet state
then manySlices (makeSlice state) (faultSet state)
else makeSlice state (cur (redexes state))
,definitionM)])
redexMark :: FileNode -> State -> Mark
redexMark red state =
(if showFaultSet state && red == faultSetRoot state
then (combMarks definitionM) else id) $
(if red == cur (redexes state) then (combMarks currentM) else id) $
(if red `elemSet` correct state
then correctM
else if red `elemSet` wrong state
then if redexFaulty red state then faultyM else wrongM
else if red `elemSet` neither state || suspectedRedex red
then markedM
else correctM)
equationText :: State -> Int -> FileNode -> String
equationText state no eqn =
leftWidth 2 (show no) ++ ". " ++ mark2Highlights (redexMark eqn state) ++
take (col (window state)-5)
(removeHighlights (prettyEquation2 initialOptions eqn))
++ highlightOff
-- need to remove highlights in equation, because otherwise
-- highlights not correctly nested and line cut off too early
leftWidth width = reverse . take width . (++ repeat ' ') . reverse
removeHighlights :: String -> String
removeHighlights xs = ys ++
if null zs then [] else (removeHighlights . tail . dropWhile (/= 'm')) zs
where
(ys,zs) = span (/= '\ESC') xs
defaultMessage :: State -> String
defaultMessage state =
"Call " ++ show (curIndex (redexes state)) ++
"/" ++ show (lengthCursorSeq (redexes state)) ++ " | " ++
(if showFaultSet state then "faulty slice" else "definition") ++ " | " ++
(if showPart state then "executed part"
else "complete")
force :: [a] -> [a]
-- force evaluation of whole list spine
force = reverse . reverse
cutOffText :: Int -> Text -> String
-- cut off text at given width, handling embedded escape sequences correctly
-- (not all, e.g. not goto)
-- also does not cut off escape sequences appearing just after width
-- incomplete lines are filled up with spaces
cutOffText mw = init . unlines . (map (cutOffLine mw))
where
cutOffLine :: Int -> String -> String
cutOffLine w xs = case xs of
"" -> replicate w ' '
(c:cs) -> if c == '\ESC'
then let (escapeSeq,end:rest) = break isAlpha cs
in c : escapeSeq++[end]++cutOffLine w rest
else if w == 0
then highlightOff
else c : cutOffLine (w-1) cs
display :: State -> IO ()
display state = do
let lastRow = firstRow state + row (window state) - srcRow - 1
tags = fileTags state
markedText = markText (text (cur (modules state))) (firstRow state)
lastRow tags
eqns = reverse (cur (redexes state) : take (eqnNo-1) (parents state))
firstEqnNo = length (parents state)-length eqns+2
putStr (home ++ lineWrap False)
writeFile "test.txt" (show tags)
(putStr . force . cutOffText (col (window state))) $
-- force minimises flickering of screen output
highlight [Bold] ("==== Hat-Explore " ++ hatVersionNumber ++ " ==== "
++ (if null (message state) then defaultMessage state
else message state)
++ ' ':repeat '=')
: zipWith (equationText state) [firstEqnNo..] eqns
++ replicate (eqnNo - length eqns) ""
++ highlight [Bold] ("---- "++filename (cur (modules state))
++" ---- lines "++show (firstRow state)++" to "
++show lastRow++' ':repeat '-')
: markedText
putStr home
data SrcModule = SrcModule
{ filename :: String
, text :: Text
, len :: Row
, border :: Slice
}
eqnNo = 5 :: Int
srcRow = eqnNo+2 :: Int
srcRows state = row (window state) - srcRow
data State = State
{ modules :: CursorSeq SrcModule -- ordered alphabetically
, window :: Coord
, firstRow :: Row
, parents :: [FileNode] -- ancestors of redexes
, redexes :: CursorSeq FileNode
-- current redex + siblings ordered by location
, correct :: Set FileNode -- trusted ones not listed
, wrong :: Set FileNode
, neither :: Set FileNode -- subset of trusted ones
, faultSetRoot :: FileNode
, faultSet :: Set FileNode
, showFaultSet :: Bool
, showPart :: Bool
, message :: String
}
newRedex :: State -> FileNode -> State
-- set given redex in state; assume parents are already set correctly
-- note: a parent might be a pseudo-parent of a constant
newRedex state new =
case parents state of
(parent:_) -> let parChildren = expSort (redexAllChildren parent)
(left,rest) = break (== new) parChildren
in if null rest -- redex not amongst children of parent
then state{redexes = mkCursorSeq [] new []}
else state{redexes = mkCursorSeq left new (tail rest)}
[] -> state{redexes = mkCursorSeq [] new []}
help :: IO ()
help = do
putStr (cls ++ goto 1 1)
putStr (highlight [Bold] "==== Help Text ===============================")
putStr (goto 1 2)
putStr helpText
getChar
return ()
where
helpText = " cursor down follow current call\n cursor up go back to caller of current call\n cursor left go to call further left in current definition body\n cursor right go to call further right in current definition body\n\n c declare current equation to be correct (wrt. intentions)\n w declare current equation to be wrong (wrt. intentions)\n n undo declaration of correctness (neither correct nor wrong)\n a amnesia - forget all declarations as correct or wrong\n\n f toggle between showing fault set or just current definition\n p toggle between showing used part of definition or full\n\n < change to alphabetically preceeding module\n > change to alphabetically succeeding module\n\n t scroll source window to top of code\n u scroll source window upwards\n d scroll source window downwards\n b scroll source window to bottom of code\n\n r redraw everything after change of window size\n h or ? display this help text\n\n q quit\n\n\nMeaning of colours:\ngreen - correct, amber - wrong, blue - unkown, red - faulty.\n\n Press any key to continue. "
loop :: State -> IO ()
loop state1 = do
let state2 = updRedexFaultyDescendants state1
display state2
let state = state2{message = ""}
c <- getChar
case c of
'q' -> putStr cls
'u' -> let oldRow = firstRow state
in loop state{firstRow = if oldRow>1 then oldRow-1 else oldRow}
'd' -> let oldRow = firstRow state
in loop state{firstRow = if oldRow<=len (cur (modules state))
then oldRow+1
else oldRow}
't' -> loop state{firstRow = 1}
'b' -> loop state{firstRow = max 1
(len (cur (modules state))
-(row (window state))+2)}
'r' -> do
(width,height) <- getTerminalSize
loop state{window=Coord{row=height,col=width}}
'c' -> loop (declareCorrect state)
'w' -> loop (declareWrong state)
'n' -> loop (declareNeither state)
'a' -> loop state{correct=emptySet,wrong=emptySet,neither=emptySet}
'f' -> loop state{showFaultSet=not (showFaultSet state)}
'p' -> loop state{showPart = not (showPart state)}
'<' -> if lengthCursorSeq (modules state) == 1
then loop state{message = "There is only one module."}
else loop state{modules = rotateLeft (modules state)}
'>' -> if lengthCursorSeq (modules state) == 1
then loop state{message = "There is only one module."}
else loop state{modules = rotateRight (modules state)}
'o' -> do
System.Cmd.system (hatObserve
(dropHS (filename (cur (modules state))))
(head (words (removeHighlights -- crude approximation
(prettyEquation2 initialOptions (cur (redexes state)))))))
loop state{message = "Spawned hat-observe"}
'l' -> do
System.Cmd.system (hatTrail (dropHS (filename (cur (modules state))))
(cur (redexes state)))
loop state{message = "Spawned hat-trail"}
'e' -> do
System.Cmd.system (hatDetect
(dropHS (filename (cur (modules state))))
(cur (redexes state)))
loop state{message = "Spawned hat-detect"}
'i' -> do
System.Cmd.system (hatAnim (dropHS (filename (cur (modules state))))
(cur (redexes state)))
loop state{message = "Spawned hat-anim"}
'h' -> help >> loop state
'?' -> help >> loop state
'\ESC' -> do
c <- getChar
if c `elem` "[O" then do
c <- getChar
case c of
'D' -> -- cursor left
if lengthCursorSeq (redexes state) == 1
then loop state{message = "The call has no siblings."}
else loop state{redexes = rotateLeft (redexes state)}
'C' -> -- cursor right
if lengthCursorSeq (redexes state) == 1
then loop state{message = "The call has no siblings."}
else loop state{redexes = rotateRight (redexes state)}
'A' -> -- cursor up
if not (null (parents state)) then checkModules
(newRedex state{parents = tail (parents state)}
(head (parents state)))
else loop state{message =
"This is a top-level constant whose caller is unknown."}
'B' -> -- cursor down
-- TO DO: handle constDef differently
case expSort (redexAllChildren (cur (redexes state))) of
(child:_) ->
checkModules (newRedex
state{parents = cur (redexes state) : parents state}
child)
[] -> loop state{message = "There are no child calls."}
else loop state{message = "Unkown command. Press h for help."}
_ -> loop state{message = "Unkown command. Press h for help."}
loadModule :: FilePath -> IO SrcModule
loadModule filename = do
filecontent <- readFile filename
let srcText = lines (expandTabs 0 filecontent)
return (SrcModule
{ filename = filename
, text = srcText
, len = length srcText
, border = borderSlice srcText})
expandTabs :: Col -> String -> String
expandTabs _ "" = ""
expandTabs c (x:xs) = case x of
'\t' -> replicate spaces ' ' ++ expandTabs (c+spaces) xs
'\n' -> '\n' : expandTabs 0 xs
'\r' -> expandTabs 0 xs
'\v' -> '\n' : expandTabs 0 xs
_ -> x : (expandTabs $! (c+1)) xs
where
spaces = 8 - c `mod` 8
checkModules :: State -> IO ()
-- called after redex was changed
-- ensure that module of redex use is loaded and becomes current Module
checkModules state = do
newModules <- insertModule (cur (redexes state)) (modules state)
loop (checkRow state{modules = newModules})
insertModules :: [FileNode] -> CursorSeq SrcModule -> IO (CursorSeq SrcModule)
insertModules [] modules = return modules
insertModules (n:ns) modules = do
modules2 <- insertModule n modules
insertModules ns modules2
insertModule :: FileNode -> CursorSeq SrcModule -> IO (CursorSeq SrcModule)
-- ensure that module of given node is loaded and becomes current one
insertModule node oldModules =
let curFilename = fst (getLocation node)
in case selectCurrent ((== curFilename) . filename) oldModules of
Just newModules -> return newModules
Nothing -> do
current <- loadModule curFilename
return (insertNewCurrent
((< curFilename) . filename) current oldModules)
checkRow :: State -> State
-- insure that redex use (or def if no use) is within the displayed window
checkRow state =
if firstInter >= firstRow state &&
lastInter < firstRow state + rows
then state
else if modLen <= rows
then state{firstRow = 1}
else if lastInter - firstInter >= rows
then state{firstRow = firstInter}
else let start = firstInter - (rows - (lastInter-firstInter+1)) `div` 2
in if start + rows -1 > modLen
then state{firstRow = modLen - rows + 1}
else if start < 1
then state{firstRow = 1}
else state{firstRow = start}
where
rows = srcRows state
modLen = len (cur (modules state))
(firstInter,lastInter) = rowsOfInterest state
rowsOfInterest :: State -> (Row,Row)
rowsOfInterest state =
if hasUseLocation (cur (redexes state))
then (row (begin (head siblingLocations))
,row (end (last siblingLocations)))
else (row (begin curLoc),row (end curLoc))
where
(_,curLoc) = getLocation (cur (redexes state))
Slice siblingLocations =
foldr mergeSlices emptySlice
(map redexUseSlice
(cursorSeqToList (redexes state)))
declareCorrect :: State -> State
declareCorrect state@State{correct=cor,wrong=wr,neither=nei} =
let red = cur (redexes state)
in state{correct = if suspectedRedex red then insertSet red cor else cor
,wrong = removeSet red wr
,neither = removeSet red nei}
declareWrong :: State -> State
declareWrong state@State{correct=cor,wrong=wr,neither=nei} =
let red = cur (redexes state)
in state{correct = removeSet red cor
,wrong = insertSet red wr
,neither = removeSet red nei}
declareNeither :: State -> State
declareNeither state@State{correct=cor,wrong=wr,neither=nei} =
let red = cur (redexes state)
in state{correct = removeSet red cor
,wrong = removeSet red wr
,neither = if suspectedRedex red then nei else insertSet red nei}
redexFaulty :: FileNode -> State -> Bool
-- cannot otherwise keep track of faulty redexes, because don't know
-- pseudoparents of a constant.
redexFaulty red state =
red `elemSet` (wrong state) &&
all (flip redexCorrect state) (redexAllChildren red)
redexCorrect :: FileNode -> State -> Bool
redexCorrect red state =
red `elemSet` correct state ||
not (suspectedRedex red || red `elemSet` neither state ||
red `elemSet` wrong state)
-----------------------------
type Text = [String] -- list of tab and newline-free lines; 1 char = 1 col
type MarkedText = [String] -- may include escape sequences for highlighting
mark2Highlights :: Mark -> String
mark2Highlights Empty = highlightOn [Normal] -- default marking
mark2Highlights (Mark hi1 hi2 ) =
highlightOn (Normal : hi1 ++ hi2)
-- the foreground colour is never changed
-- this may be problematic if default foreground does not contrast with
-- the various background colours chosen here
-- however, some terminals (e.g. kde) interpret bold + foreground colour
-- as a lighter foreground colour; so to obtain bold plus background colour
-- also on these terminals, the foreground is never changed.
markText :: Text -> Row -> Row -> [Tag] -> MarkedText
-- markup input text from first row up to second row
-- result only contains the specified rows
-- filled up with empty rows if necessary
-- assumes at least first row and rows mentioned in tags exist in text
markText text beginRow endRow tags =
markTextTo startText startTags endRow
where
startText = drop (beginRow-1) text
startTags = getTagsFromRow tags beginRow
markTextTo :: Text -> [Tag] -> Row -> MarkedText
-- assumes that coords of first tag are coords of first text char
markTextTo text (t:ts) lastRow =
(mark2Highlights (mark t)++line):lines
where
(line:lines) = go text (start t) ts
go :: Text -> Coord -> [Tag] -> MarkedText
go (line:lines) current (t:ts) =
if row (start t) > lastRow then
copyLastLines (line:lines) (row current)
else if row (start t) == row current then
let (firstLine,restLine) =
splitAt (col (start t) - col current) line
(mRestLine:mRest) = go (restLine:lines) (start t) ts
in (firstLine ++ mark2Highlights (mark t) ++ mRestLine)
: mRest
else -- row (start t) > row current
line : go lines Coord{row = row current+1,col = 1} (t:ts)
go lines current _ = -- either no lines or no tags
copyLastLines lines (row current)
copyLastLines :: Text -> Row -> MarkedText
copyLastLines (line:lines) currentRow =
if currentRow < lastRow then
line : copyLastLines lines (currentRow+1)
else -- currentRow == lastRow
[line ++ highlightOff]
copyLastLines [] currentRow =
replicate (lastRow-currentRow-1) [] ++ [highlightOff]
splitText :: Text -> Coord -> Coord -> (Text,Text)
-- all texts start and end with incomplete rows
splitText (line:lines) first second =
if row first == row second then
let (firstLine,restLine) = splitAt (col second - col first) line
in ([firstLine],restLine:lines)
else -- row first < row second
let (firstText,secondText) = splitText lines Coord{row = row first+1,col = 1} second
in (line:firstText,secondText)
getTagsFromRow :: [Tag] -> Row -> [Tag]
-- first tag starts at beginning of the row
getTagsFromRow tags first = go tags noneM
where
go [] m = [Tag{start=Coord{row=first,col=1},mark=m}]
go (t:ts) m = case compare (row (start t)) first of
LT -> go ts (mark t)
EQ -> if col (start t) == 1 then t:ts
else
Tag{start=Coord{row=first,col=1},mark=m}:t:ts
GT -> Tag{start=Coord{row=first,col=1},mark=m}:t:ts
data Mark = Empty | Mark [Highlight] [Highlight] deriving Show -- TESTING
-- second (possibly) contains background colour
-- first contains all other highlighting
-- they are separate to simplify combination (does it??)
emptyM = Empty
currentM = Mark [Underscore,Bold] []
markedM = Mark [] [Background Cyan]
correctM = Mark [] [Background Green]
wrongM = Mark [] [Background Yellow]
faultyM = Mark [] [Background Red]
definitionM = Mark [Bold] []
noneM = Mark [] []
combMarks :: Mark -> Mark -> Mark
combMarks Empty _ = Empty
combMarks _ Empty = Empty
combMarks (Mark hi11 hi12) (Mark hi21 hi22) =
Mark (hi11 `union` hi21) (hi12 `union` hi22)
data Tag = Tag {start :: Coord, mark :: Mark} deriving Show -- TESTING
combineSlices :: [(Slice,Mark)] -> [Tag]
combineSlices = foldr1 combTags . map (uncurry slice2Tags)
slice2Tags :: Slice -> Mark -> [Tag]
slice2Tags slice m = enter (unSlice slice)
where
enter :: [Location] -> [Tag]
enter [] = []
enter (l:ls) = if end l < begin l
then enter ls
else Tag{start = begin l, mark = m} : exit (l:ls)
exit :: [Location] -> [Tag]
exit (l:ls) = Tag{start= afterCoord (end l), mark = noneM} : enter ls
combTags :: [Tag] -> [Tag] -> [Tag]
combTags t1s t2s = go noneM t1s noneM t2s
where
go :: Mark -> [Tag] -> Mark -> [Tag] -> [Tag]
-- keep track of current mark for each tag sequence
go m1 [] _ ts = map (\t -> t{mark = combMarks m1 (mark t)}) ts
go _ ts m2 [] = map (\t -> t{mark = combMarks m2 (mark t)}) ts
go m1 (t1:t1s) m2 (t2:t2s) = case compare (start t1) (start t2) of
LT -> Tag{start = start t1, mark = combMarks (mark t1) m2}
: go (mark t1) t1s m2 (t2:t2s)
GT -> Tag{start = start t2, mark = combMarks (mark t2) m1}
: go m1 (t1:t1s) (mark t2) t2s
EQ -> Tag{start = start t1, mark = combMarks (mark t1) (mark t2)}
: go (mark t1) t1s (mark t2) t2s
-------------------------------------------------------
newtype Slice = Slice {unSlice :: [Location]}
-- proper locations, non-overlapping and ordered
compressSlice :: Slice -> Slice
compressSlice (Slice []) = Slice []
compressSlice (Slice [l]) = Slice [l]
compressSlice (Slice (l1:l2:ls)) =
if isNextCoord (end l1) (begin l2) then
compressSlice (Slice (Location{begin = begin l1, end = end l2}:ls))
else
Slice (l1 : unSlice (compressSlice (Slice (l2:ls))))
mergeSlices :: Slice -> Slice -> Slice
-- additive combination of two slices
mergeSlices rs (Slice []) = rs
mergeSlices (Slice []) rs = rs
mergeSlices (Slice (l1:l1s)) (Slice (l2:l2s)) =
if end l1 < begin l2 then -- location 1 before location 2
Slice (l1 : (unSlice (mergeSlices (Slice l1s) (Slice (l2:l2s)))))
else if end l2 < begin l1 then -- location 2 before location 1
Slice (l2 : (unSlice (mergeSlices (Slice (l1:l1s)) (Slice l2s))))
else if end l1 < end l2 then
mergeSlices
(Slice l1s)
(Slice (Location{begin = min (begin l1) (begin l2),end = end l2} : l2s))
else -- end l2 <= end l1
mergeSlices
(Slice (Location{begin = min (begin l1) (begin l2),end = end l1} : l1s))
(Slice l2s)
addToSlice :: Location -> Slice -> Slice
-- maybe a more efficient implementation will be done later
addToSlice loc = mergeSlices (Slice [loc])
subtractFromSlice :: Location -> Slice -> Slice
-- given location must be improper or
-- nested in one of the locations of the slice
-- or be outside any locations of the slice; no overlap
subtractFromSlice l (Slice []) = emptySlice
subtractFromSlice l1 (Slice (l2:ls)) =
if isImproperLocation l1 then
Slice (l2:ls)
else if begin l1 > end l2 then -- l1 not nested in l2
Slice (l2 : unSlice (subtractFromSlice l1 (Slice ls)))
else -- from assumption of nesting follows that l1 nested in l2
if begin l1 == begin l2 then
if end l1 == end l2 then
Slice ls
else
Slice (Location{begin=afterCoord (end l1),end=end l2} : ls)
else
if end l1 == end l2 then
Slice (Location{begin=begin l2,end=beforeCoord (begin l1)} : ls)
else
Slice (Location{begin=begin l2,end=beforeCoord (begin l1)} :
Location{begin=afterCoord (end l1),end=end l2} : ls)
mergeSlicesMap :: (a -> Slice) -> [a] -> Slice
mergeSlicesMap f ns = foldr mergeSlices emptySlice (map f ns)
manySlices :: (FileNode -> Slice) -> Set FileNode -> Slice
manySlices f set = foldr mergeSlices emptySlice (map f (deSet set))
emptySlice :: Slice
emptySlice = Slice []
mkSlice :: Location -> Slice
mkSlice l = if isVirtLocation l || isNoLocation l then Slice [] else Slice [l]
-------------------------------------------------------------------
-- abstract data type ordered sequence of elements with one current element
data CursorSeq a = CursorSeq {cur :: a, left :: [a], right :: [a]}
-- describes sequence with elements ordered as follows:
-- reverse left ++ cur : right
-- with one element marked as current element
lengthCursorSeq :: CursorSeq a -> Int
lengthCursorSeq cs = 1 + length (left cs) + length (right cs)
cursorSeqToList :: CursorSeq a -> [a]
cursorSeqToList cs = reverse (left cs) ++ cur cs : right cs
mkCursorSeq :: [a] -> a -> [a] -> CursorSeq a
mkCursorSeq l c r = CursorSeq{cur = c, left = reverse l, right = r}
insertNewCurrent :: (a -> Bool) -> a -> CursorSeq a -> CursorSeq a
-- predicate is true for all smaller elements
insertNewCurrent p c cs = let
elements = reverse (left cs) ++ cur cs : right cs
(l,r) = span p elements
in mkCursorSeq l c r
curIndex :: CursorSeq a -> Int
curIndex cs = length (left cs) + 1
selectCurrent :: (a -> Bool) -> CursorSeq a -> Maybe (CursorSeq a)
-- may not find element fulfilling predicate
selectCurrent p cs = let
elements = reverse (left cs) ++ cur cs : right cs
(l,r) = break p elements
in case r of
[] -> Nothing
(c:r') -> Just (mkCursorSeq l c r')
rotateLeft :: CursorSeq a -> CursorSeq a
-- jumps at end
rotateLeft cs = let l = left cs in
if null l
then if null (right cs)
then cs
else let r = reverse (cur cs:right cs) in
CursorSeq{cur = head r, left = tail r, right = []}
else CursorSeq{cur = head l, left = tail l, right = cur cs:right cs}
rotateRight :: CursorSeq a -> CursorSeq a
-- jumps at end
rotateRight cs = let r = right cs in
if null r
then if null (left cs)
then cs
else let l = reverse (cur cs:left cs) in
CursorSeq{cur = head l, left = [], right = tail l}
else CursorSeq{cur = head r, left = cur cs:left cs, right = tail r}
-------------------------------------------------------------------
-- abstract data type set
newtype Set a = Set {deSet :: [a]}
emptySet :: Set a
emptySet = Set []
insertSet :: Eq a => a -> Set a -> Set a
insertSet x set = if x `elemSet` set then set else Set (x: deSet set)
elemSet :: Eq a => a -> Set a -> Bool
elemSet x set = x `elem` deSet set
removeSet :: Eq a => a -> Set a -> Set a
removeSet x set = Set [y | y <- deSet set, y /= x]
subSet :: Eq a => Set a -> Set a -> Bool
subSet s1 s2 = all (`elemSet` s2) (deSet s1)
unionSet :: Eq a => Set a -> Set a -> Set a
unionSet s1 s2 = foldr insertSet s2 (deSet s1)
-------------------------------------------------------------------