cnc-spec-compiler-0.2.0.0: Intel/Cnc/Spec/Passes/ReadHarch.hs
{-# LANGUAGE RecordWildCards, TupleSections, ScopedTypeVariables, DeriveFunctor #-}
----------------------------------------------------------------------------------------------------
-- Read .harch profiled/partitioned graph files.
--
-- Original Author: Ryan Newton
----------------------------------------------------------------------------------------------------
module Intel.Cnc.Spec.Passes.ReadHarch
-- (
-- readHarchFile, parseHarchFile,
-- HarchNode (..), showTreePath,
-- test_readharch
-- )
where
import Intel.Cnc.Spec.CncGraph
import Intel.Cnc.Spec.Util
import Test.HUnit
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.Combinator
import Text.Parsec.String
import System.IO
import Data.List
--import Data.Set as S hiding (map, filter, partition)
import Data.Set (Set, member)
import qualified Data.Set as Set
import Data.List.Split (splitOn) -- from 'split' package
import Data.Graph
import Debug.Trace
import Data.Function
import qualified Control.Exception as CE
import qualified Data.Graph.Inductive as G
import qualified Data.Graph.Inductive.Query.DFS as DFS
import Data.Graph.Inductive.Query.Monad (mapFst, mapSnd)
----------------------------------------------------------------------------------------------------
-- A graph from a Harch file is currently not quite the same datatype as a CncGraph.
-- The HarchGraph has only step collections currently. It has no edge labels.
data HarchSpec = HarchSpec {
hgraph :: HarchGraph,
htree :: HarchTreeOrdered
}
type HarchGraph = G.Gr HarchNode ()
-- An individual node within the graph:
data HarchNode = HarchNode {
name :: String, -- A mandatory field.
properties :: [(String, String)], --
num :: Int, -- Using numeric idenifiers for now
nodecomments :: [String] -- User comments or documentation.
}
deriving Show
-- | A datatype for Harch trees, e.g. for hierarchical decompositions.
-- Parameterized by the type of the partitions themselves.
--
-- This is sometimes a redundant representation because not all of the
-- intermediate nodes need to be labeled with the full contents. In
-- fact, for HarchTreeUnordered only leaves need be labeled. But
-- HarchTreeOrdered encodes extra information at all the intermediate
-- nodes (namely, potentially unique orderings).
data HarchTree a = HT a [HarchTree a]
deriving (Show, Eq, Ord, Functor)
-- A simple representation to start with is a set of Ints for each partition:
type HarchTreeUnordered = HarchTree (Set Int)
-- After topological sorting the partitions are ordered.
type HarchTreeOrdered = HarchTree [Int]
-- | Read a Harch file from disk.
readHarchFile :: String -> IO HarchSpec
readHarchFile path =
do file <- openFile path ReadMode
txt <- hGetContents file
return (parseHarchFile txt)
-- | Parse the contents of a Harch file stored in a string.
--parseHarchFile :: String -> (HarchGraph, HarchTreeUnordered)
parseHarchFile :: String -> HarchSpec
parseHarchFile txt = HarchSpec gr ordered
where
ls = run harchfile txt
gr = convertHarchGraph ls
sorted = DFS.topsort gr
part = extractPartitions ls
ordered = fmap (\set -> filter (flip member set) sorted) part
----------------------------------------------------------------------------------------------------
-- Temporary datatypes used in parsing:
----------------------------------------------------------------------------------------------------
-- A simple datatype for parsed Harch Nodes:
data HarchNodeParse = HNP {
hnode :: HarchNode,
in_edges :: [Int],
out_edges :: [Int]
}
deriving Show
----------------------------------------------------------------------------------------------------
-- Parsec parser for .harch syntax:
-- We use METIS comment lines for two purposes. Normal comments, and the reserved HARCHNODE lines.
-- Maybe we should have used a different delimiter, like %% for harch nodes to avoid this backtracking.
commentline :: Parser String
commentline = try $
do whitespc; char '%'; whitespc
notFollowedBy (string "HARCHNODE")
str <- many (noneOf "\n")
newline
return str
harchfile :: Parser [HarchNodeParse]
harchfile =
do global_comments <- many commentline
numbers; newline -- Skip the first line
nodes <- many harchnode
return$ map (\ (n, rec) ->
let h = hnode rec in rec { hnode= h{ num= n } })
(zip [1..] nodes)
-- Remove one property from a property list and return the remainder.
popProp :: String -> [(String, String)] -> (String, [(String, String)])
popProp name pls =
case partition ((== name) . fst) pls of
([(_,nm)], rest) -> (nm, rest)
(ls,_) -> error$ "Currently, exactly one '"++ show name ++"' property is required for each graph node, not "
++ show (length ls)++ ": "++ show ls
-- Remove a property that may occur multiple times:
popMultiProp name pls =
error "TODO, popMultiProp: implement me"
harchnode :: Parser HarchNodeParse
harchnode =
do
node_comments1 <- many commentline
whitespc; char '%'; whitespc; string "HARCHNODE"; whitespc
node_comments2 <- many commentline
ps <- props; newline;
(wght:edges) <- numbers; whitespc; newline;
-- Parse out all the special fields:
let (nm, rest) = popProp "name" ps
-- Are the edges mentioned inbound or outbound?
let (dirs,rest2) = popProp "directions" rest
let (ins,outs) =
if not (length dirs == length edges)
then error$ "\nNumber of 'directions' incorrect, "++ show (length dirs) ++" "++
show dirs ++ " expected "++ show (length edges) ++ " for edges " ++ show edges
else (map snd $ filter ((== '0') . fst) $ zip dirs edges,
map snd $ filter ((== '1') . fst) $ zip dirs edges)
return HNP {
hnode = HarchNode { name= nm, properties= rest2, num=0,
nodecomments = node_comments1 ++ node_comments2 },
in_edges= ins, out_edges= outs
}
spc = oneOf " \t"
whitespc = many spc
-- Parse one property in the property list:
prop :: Parser (String,String)
prop = do w <- many1 letter
char '=';
-- The property may be the empty string:
p <- many (noneOf ";")
char ';';
return (w,p)
-- Having problems with sepBy:
props :: Parser [(String,String)]
props = prop `sepEndBy` (many1 spc)
numbers :: Parser [Int]
numbers = do whitespc
strs <- (many1 digit) `sepEndBy` (many1 spc)
return$ map read strs
------------------------------------------------------------
-- Generic harness for running a parser:
run :: Show a => Parser a -> String -> a
run p input
= case (parse p "" input) of
Left err -> error ("parse error at "++ show err)
Right x -> x
----------------------------------------------------------------------------------------------------
-- Conversion to partitioned format:
showTreePath :: [Int] -> String
showTreePath intls = concat$ intersperse ":" $ map show intls
-- Parse the partition information packed into each vertices' metadata
extractPartitions :: [HarchNodeParse] -> HarchTreeUnordered
extractPartitions parsednodes =
--trace ("Allnums: "++ show allnums)$
build (allnums, sorted)
where
nodes = map hnode parsednodes
allnums :: Set Int
allnums = Set.fromList$ map num nodes
sorted :: [([Int], HarchNode)]
sorted = sortBy (\ a b -> fst a `compare` fst b) $
map extract nodes
-- This pulls out the tree index (e.g. "0:1:3:0") as an int list:
extract :: HarchNode -> ([Int], HarchNode)
extract (nd@HarchNode{..}) =
let (partstr, rest) = popProp "partitions" properties in
(map read $ filter (not . null) $ splitOn ":" partstr :: [Int],
nd { properties= rest})
-- Build a tree recursively:
build :: (Set Int, [([Int], HarchNode)]) -> HarchTreeUnordered
-- build (set, paths) | Set.null set =
build (set, paths) =
-- Each group is a child partition
let
still_here = filter (not . null . fst) paths -- remove those that ran out
sorted = sortBy (compare `on` (head . fst)) still_here
-- Group by the head-index and then chop it off.
grouped = groupBy ((==) `on` (head . fst)) sorted
clipped = map (map (mapFst tail)) grouped
-- For each sub-partition cut down the node set.
restricted = map (Set.intersection set) $
map (Set.fromList . map (num . snd)) clipped
tree_children = map build $ zip restricted clipped
-- Also sanity check those head-indexes.
heads = map (head . fst . head) grouped
shownpaths =
"heads " ++ show heads ++ " of paths "++
concat (intersperse " "$ map showTreePath$ map fst paths)
checked =
(if length heads == 1
then trace ("WARNING: Degenerate sub-partition is equal to parent partition: "++
shownpaths)
else id) $
-- FIXME: expensive way of checking:
(if null heads || (minimum heads == 0 && heads == [0..maximum heads])
then tree_children
else trace ("WARNING: Tree-indices of sub-partitions are irregular; should be consecutive range [0,N).\n"++
" Instead received "++ shownpaths)
tree_children)
in HT set checked
-- Convert parsed adjacency list info into a real graph.
convertHarchGraph :: [HarchNodeParse] -> HarchGraph
convertHarchGraph parsednodes =
-- Quick sanity check, catch things before the HORRIBLE fgl errors do.
if Set.null diff
then G.mkGraph vertices edges
--trace (" Vertices "++ show (map fst vertices) ++"\n Edges "++ show edges) $
else error$ "Edges to connect to nonexistent nodes! " ++ show (Set.toList diff)
where
nodes = map hnode parsednodes
vertices = zip nums nodes
edges = concat (map (\ nd -> map (num (hnode nd), , ()) $ out_edges nd)
parsednodes)
nums = map num nodes
all_edges = Set.fromList$ concat$ map (\ (a,b,_) -> [a,b]) edges
diff = Set.difference all_edges (Set.fromList nums)
----------------------------------------------------------------------------------------------------
-- Testing: Or at least some miscellaneous unit tests.
----------------------------------------------------------------------------------------------------
runPr prs str = print (run prs str)
t1 = run prop$ "name=foo;"
t2 = run props$ "name=foo; direction=01;" -- No spcs at start end
t3 = run props$ "name=foo;"
t4 = run props$ "name=foo; blah=baz; "
t5 = run props$ "name=; blah=; " -- Blank properties
l1 = "% HARCHNODE name=blah; direction=01;\n"
l2 = " 0 1 2 "
t6 = run numbers$ l2
foo = do whitespc; newline
bar = do char '\n'
t7 = run foo " \n"
t8 = run foo "\n"
t9 = run bar "\n"
t11 = runPr harchnode$ l1 ++ l2 ++ "\n"
testfile = unlines $
["4 3 10",
"% graph with 4 verts 3 edges",
"% HARCHNODE name=foo; directions=11; partitions=1; index=1;",
"1 2 3",
"% HARCHNODE name=bar; directions=; partitions=1:0; index=2;",
"1",
"% HARCHNODE name=baz1; directions=1; partitions=1:1; index=3;",
"1 4",
"% HARCHNODE name=baz2; directions=; partitions=1:1; index=4;",
"1"]
t12 = runPr harchfile testfile
test_readharch =
testSet "ReadHarch" $
[
testCase "" "simple parse test 1"$ ("name","foo") ~=? t1
, testCase "" "simple parse test 2"$ [("name","foo"),("direction","01")] ~=? t2
, testCase "" "simple parse test 3"$ [("name","foo")] ~=? t3
, testCase "" "simple parse test 4"$ [("name","foo"),("blah","baz")] ~=? t4
, testCase "" "simple parse test: blank props"$ [("name",""),("blah","")] ~=? t5
, testCase "" "parse numbers"$ [0,1,2] ~=? t6
, testCase "" "whitespace newline 1"$ '\n' ~=? t7
, testCase "" "whitespace newline 2"$ '\n' ~=? t8
, testCase "" "whitespace newline 3"$ '\n' ~=? t9
, testCase "" "expected parse error"$ TestCase $ do
CE.catch (do t11; assertFailure "Parse of incorrect syntax must return an error")
(\ (e :: CE.SomeException) -> return ())
, testCase "" "commentline 1"$ "blah blah blah" ~=? run commentline " % blah blah blah\n"
, testCase "" "commentline 2"$ "HARCHNO blah" ~=? run commentline " % HARCHNO blah\n"
, testCase "" "commentline 3, expect fail"$ TestCase $ do
CE.catch (do runPr commentline " % HARCHNODE foo \n"
assertFailure "Parse of HARCHNODE line must not satisfy commentline.")
(\ (e :: CE.SomeException) -> return ())
, testCase "" "parse complete file, and print to stdout"$ TestCase t12
]
----------------------------------------------------------------------------------------------------