| 1 | |
{-# LANGUAGE ParallelListComp #-}
|
| 2 | |
|
| 3 | 1 |
module Main where
|
| 4 | 2 |
|
| 5 | |
import Data.List (intercalate)
|
| 6 | |
import qualified Data.Text as T
|
| 3 |
import Control.Error
|
| 7 | 4 |
import Data.Text (Text)
|
| 5 |
import qualified Data.Text.IO as T
|
| 6 |
import System.Environment (getArgs)
|
| 7 |
import System.IO ( IOMode(WriteMode)
|
| 8 |
, hClose
|
| 9 |
, openFile
|
| 10 |
, stdout
|
| 11 |
)
|
| 8 | 12 |
|
| 9 | |
import AST
|
| 13 |
import AST (Recipe)
|
| 14 |
import BuildTree (showDotGraph, showTree)
|
| 10 | 15 |
import Parser (parseFile)
|
| 11 | 16 |
|
| 17 |
usage :: String
|
| 18 |
usage =
|
| 19 |
unlines ("\n\nCOMMANDS:" : [ " " ++ r | (r, _) <- renderers ])
|
| 20 |
|
| 21 |
renderers :: [(String, Recipe -> Text)]
|
| 22 |
renderers =
|
| 23 |
[ ("dot", showDotGraph)
|
| 24 |
, ("reverse-tree", showTree)
|
| 25 |
]
|
| 26 |
|
| 12 | 27 |
main :: IO ()
|
| 13 | |
main = do
|
| 14 | |
cs <- getContents
|
| 15 | |
case parseFile "[stdin]" cs of
|
| 16 | |
Left err -> putStrLn err
|
| 17 | |
Right recipes ->
|
| 18 | |
putStrLn (unlines (map showRecipeGraph recipes))
|
| 19 | |
|
| 20 | |
-- An 'ActionChunk' represents a set of actions in between two join
|
| 21 | |
-- points. This is 'reversed' from what we'd expect: the 'name' is
|
| 22 | |
-- actually the name of the join point at the end of a sequence of
|
| 23 | |
-- actions, or the string DONE, while the 'prev' is the name of the
|
| 24 | |
-- join point that came at the beginning, or the ingredients list
|
| 25 | |
-- that started the rule. The actions also will appear in reverse
|
| 26 | |
-- order.
|
| 27 | |
|
| 28 | |
-- Maybe an explanation is in order: this rule
|
| 29 | |
-- ingredients -> a -> $x -> b -> c -> $y;
|
| 30 | |
-- will produce two ActionChunks:
|
| 31 | |
-- ActionChunk $y [c, b] (Right $x)
|
| 32 | |
-- and
|
| 33 | |
-- ActionChunk $x [a] (Left ingredients)
|
| 34 | |
data ActionChunk = ActionChunk
|
| 35 | |
{ acName :: Text
|
| 36 | |
, acRules :: [Text]
|
| 37 | |
, acPrev :: Either IngredientList Text
|
| 38 | |
} deriving (Eq, Show)
|
| 39 | |
|
| 40 | |
-- This is the function that actually splits apart the action into
|
| 41 | |
-- ActionChunks. It's grosser than I'd hoped, but it's mostly a lot
|
| 42 | |
-- of fiddly but straightforward traversing.
|
| 43 | |
splitApart :: Either IngredientList Text -> [Action] -> [ActionChunk]
|
| 44 | |
splitApart i = toChunk [] . reverse
|
| 45 | |
where toChunk cs (Join t:xs) =
|
| 46 | |
gather t xs [] cs
|
| 47 | |
toChunk cs (Action "DONE" _:xs) =
|
| 48 | |
gather "DONE" xs [] cs
|
| 49 | |
toChunk cs (Done:xs) =
|
| 50 | |
gather "DONE" xs [] cs
|
| 51 | |
toChunk _ (Action _ _:_) =
|
| 52 | |
error "expected chunk to end with a join or DONE"
|
| 53 | |
toChunk cs [] = cs
|
| 54 | |
gather n xs@(Join t:_) as cs =
|
| 55 | |
toChunk (ActionChunk n (reverse as) (Right t) : cs) xs
|
| 56 | |
gather n (Action t _:xs) as cs =
|
| 57 | |
gather n xs (t:as) cs
|
| 58 | |
gather _ (Done:_) _ _ =
|
| 59 | |
error "unsure how to handle this case"
|
| 60 | |
gather n [] as cs =
|
| 61 | |
ActionChunk n (reverse as) i : cs
|
| 62 | |
|
| 63 | |
-- Here we take a recipe and pull all the ActionChunks into a single
|
| 64 | |
-- list.
|
| 65 | |
getChunks :: Recipe -> [ActionChunk]
|
| 66 | |
getChunks Recipe { rRecipe = st } =
|
| 67 | |
mconcat (map getActions st)
|
| 68 | |
where getActions (Step (InpJoin t) as) = splitApart (Right t) as
|
| 69 | |
getActions (Step (InpIngredients is) as) = splitApart (Left is) as
|
| 70 | |
|
| 71 | |
-- The ReverseGraph is a tree rooted at the DONE node. The 'children'
|
| 72 | |
-- are actually the steps leading up to a given node. Only childless
|
| 73 | |
-- nodes should have an IngredientList associated with them, but we
|
| 74 | |
-- don't encode this invariant in the type.
|
| 75 | |
data ReverseGraph = ReverseGraph
|
| 76 | |
{ rStep :: Either IngredientList Text
|
| 77 | |
, rPrevs :: [ReverseGraph]
|
| 78 | |
} deriving (Eq, Show)
|
| 79 | |
|
| 80 | |
-- Take a list of ActionChunks and stitch them back together so that
|
| 81 | |
-- we can build a ReverseGraph of them. Again, fiddly but straightforward
|
| 82 | |
-- traversing of the data structures.
|
| 83 | |
buildReverseGraph :: [ActionChunk] -> ReverseGraph
|
| 84 | |
buildReverseGraph as = ReverseGraph (Right "DONE")
|
| 85 | |
(concat (map buildFrom (findChunks "DONE")))
|
| 86 | |
where findChunks n = [ chunk | chunk <- as, acName chunk == n ]
|
| 87 | |
buildFrom (ActionChunk _ rs p) = go rs p
|
| 88 | |
go [] (Right p) = concat $ map buildFrom (findChunks p)
|
| 89 | |
go [] (Left i) = [ReverseGraph (Left i) []]
|
| 90 | |
go (r:rs) p = [ReverseGraph (Right r) (go rs p)]
|
| 91 | |
|
| 92 | |
-- Prettily convert a ReverseGraph to a readable tree. This will give
|
| 93 | |
-- us a recipe tree in reverse order, starting with the DONE, and
|
| 94 | |
-- gradually going back to the ingredients.
|
| 95 | |
prettyGraph :: ReverseGraph -> String
|
| 96 | |
prettyGraph = go 0
|
| 97 | |
where go n (ReverseGraph t rs) =
|
| 98 | |
indent n ++ stepName t ++ "\n" ++ concat (map (go (n+2)) rs)
|
| 99 | |
indent n = replicate n ' '
|
| 100 | |
|
| 101 | |
stepName :: Either IngredientList Text -> String
|
| 102 | |
stepName (Right t) = T.unpack t
|
| 103 | |
stepName (Left (IngredientList is)) =
|
| 104 | |
intercalate "; " [ ingName i | i <- is ]
|
| 105 | |
where ingName (Ingredient (Just amt) name) =
|
| 106 | |
T.unpack amt ++ " " ++ T.unpack name
|
| 107 | |
ingName (Ingredient Nothing name) =
|
| 108 | |
T.unpack name
|
| 109 | |
|
| 110 | |
stepMeta :: Either IngredientList Text -> String
|
| 111 | |
stepMeta (Right t) = " [label=\"" ++ T.unpack t ++ "\",color=red]"
|
| 112 | |
stepMeta (Left (IngredientList is)) =
|
| 113 | |
" [label=\"" ++ intercalate "; " [ ingName i | i <- is ] ++ "\"]"
|
| 114 | |
where ingName (Ingredient (Just amt) name) =
|
| 115 | |
T.unpack amt ++ " " ++ T.unpack name
|
| 116 | |
ingName (Ingredient Nothing name) =
|
| 117 | |
T.unpack name
|
| 118 | |
|
| 119 | |
dotGraph :: Text -> ReverseGraph -> String
|
| 120 | |
dotGraph rname gr = ("digraph \"" ++ T.unpack rname ++ "\" {\n") ++ unlines (go "n" 0 gr) ++ "\n}"
|
| 121 | |
where go :: String -> Int -> ReverseGraph -> [String]
|
| 122 | |
go parent n (ReverseGraph t rs) =
|
| 123 | |
let name = parent ++ "_" ++ show n
|
| 124 | |
children = [ (i, name ++ "_" ++ show i, r)
|
| 125 | |
| i <- [0..]
|
| 126 | |
| r <- rs
|
| 127 | |
]
|
| 128 | |
in [ " " ++ name ++ stepMeta t ] ++
|
| 129 | |
[ " " ++ cname ++ " -> " ++ name ++ ";"
|
| 130 | |
| (_, cname, _) <- children
|
| 131 | |
] ++
|
| 132 | |
concat [ go name i r
|
| 133 | |
| (i, _, r) <- children
|
| 134 | |
]
|
| 135 | |
|
| 136 | |
showRecipeGraph :: Recipe -> String
|
| 137 | |
showRecipeGraph r@(Recipe name _) = dotGraph name . buildReverseGraph . getChunks $ r
|
| 28 |
main = runScript $ do
|
| 29 |
as <- scriptIO getArgs
|
| 30 |
(file, render, contents, output) <- case as of
|
| 31 |
[x] -> do
|
| 32 |
r <- lookup x renderers ?? ("Unable to find renderer " ++ x)
|
| 33 |
cs <- scriptIO getContents
|
| 34 |
return ("[stdin]", r, cs, stdout)
|
| 35 |
[x,f] -> do
|
| 36 |
r <- lookup x renderers ?? ("Unable to find renderer " ++ x)
|
| 37 |
cs <- scriptIO $ readFile f
|
| 38 |
return (f, r, cs, stdout)
|
| 39 |
[x,f,o] -> do
|
| 40 |
r <- lookup x renderers ?? ("Unable to find renderer " ++ x)
|
| 41 |
cs <- scriptIO $ readFile f
|
| 42 |
file <- scriptIO $ openFile o WriteMode
|
| 43 |
return (f, r, cs, file)
|
| 44 |
_ -> throwE usage
|
| 45 |
ast <- hoistEither $ parseFile file contents
|
| 46 |
mapM_ (scriptIO . T.hPutStrLn output . render) ast
|
| 47 |
scriptIO $ hClose output
|