Rearranged module to separate lib from executable
This allows Apicius to be used in other applications---for
example, a web site---much more easily.
    
    
      
        Getty Ritter
        9 years ago
      
    
    
  
  
  | 1 | module Apicius.AST where | |
| 2 | ||
| 3 | import Data.Monoid ((<>)) | |
| 4 | import Data.Text (Text) | |
| 5 | import Apicius.Util | |
| 6 | ||
| 7 | data Recipe = Recipe | |
| 8 | { rName :: Text | |
| 9 | , rRecipe :: [Step] | |
| 10 | } deriving (Eq, Show) | |
| 11 | ||
| 12 | instance TShow Recipe where | |
| 13 | text (Recipe name steps) = | |
| 14 | "Recipe { rName = " <> | |
| 15 | text name <> | |
| 16 | ", rRecipe = " <> | |
| 17 | text steps <> | |
| 18 | " }" | |
| 19 | ||
| 20 | data Step = Step | |
| 21 | { sInputs :: Input | |
| 22 | , sActions :: [Action] | |
| 23 | } deriving (Eq, Show) | |
| 24 | ||
| 25 | instance TShow Step where | |
| 26 | text (Step inp acts) = | |
| 27 | "Step { sInputs = " <> | |
| 28 | text inp <> | |
| 29 | ", sActions = " <> | |
| 30 | text acts <> | |
| 31 | " }" | |
| 32 | ||
| 33 | data Input | |
| 34 | = InpIngredients IngredientList | |
| 35 | | InpJoin Text | |
| 36 | deriving (Eq, Show) | |
| 37 | ||
| 38 | instance TShow Input where | |
| 39 | text (InpJoin ts) = text ts | |
| 40 | text (InpIngredients is) = text is | |
| 41 | ||
| 42 | data IngredientList = IngredientList | |
| 43 | { fromIngredientList :: [Ingredient] | |
| 44 | } deriving (Eq, Show) | |
| 45 | ||
| 46 | instance TShow IngredientList where | |
| 47 | text (IngredientList is) = | |
| 48 | "IngredientList " <> text is | |
| 49 | ||
| 50 | data Ingredient = Ingredient | |
| 51 | { iAmount :: Maybe Text | |
| 52 | , iType :: Text | |
| 53 | } deriving (Eq, Show) | |
| 54 | ||
| 55 | instance TShow Ingredient where | |
| 56 | text (Ingredient i t) = | |
| 57 | "Ingredient { iAmount = " <> | |
| 58 | text i <> | |
| 59 | ", iType = " <> | |
| 60 | text t <> | |
| 61 | " }" | |
| 62 | ||
| 63 | data Action | |
| 64 | = Action Text (Maybe IngredientList) | |
| 65 | | Join Text | |
| 66 | | Done | |
| 67 | deriving (Eq, Show) | |
| 68 | ||
| 69 | instance TShow Action where | |
| 70 | text (Action t i) = "Action " <> text t <> " " <> text i | |
| 71 | text (Join t) = "Join " <> text t | |
| 72 | text Done = "Done" | 
| 1 | module Apicius.Language ( Recipe(..) | |
| 2 | , Step(..) | |
| 3 | , Input(..) | |
| 4 | , IngredientList(..) | |
| 5 | , Ingredient(..) | |
| 6 | , Action(..) | |
| 7 | , parseFile | |
| 8 | , showAst | |
| 9 | ) where | |
| 10 | ||
| 11 | import Data.Text (Text) | |
| 12 | ||
| 13 | import Apicius.AST | |
| 14 | import Apicius.Parser | |
| 15 | import Apicius.Util (text) | |
| 16 | ||
| 17 | showAst :: Recipe -> Text | |
| 18 | showAst = text | 
| 1 | { | |
| 2 | {-# OPTIONS -w #-} | |
| 3 | {-# LANGUAGE OverloadedStrings #-} | |
| 4 | ||
| 5 | module Apicius.Lexer where | |
| 6 | ||
| 7 | import Control.Monad (liftM) | |
| 8 | import Data.Text (Text) | |
| 9 | import qualified Data.Text as T | |
| 10 | ||
| 11 | import Prelude hiding (lex) | |
| 12 | } | |
| 13 | ||
| 14 | %wrapper "monadUserState" | |
| 15 | ||
| 16 | $special = [ \{ \} \[ \] \( \) \; \, \+ \& \- \$ ] | |
| 17 | $idchar = $printable # $special | |
| 18 | ||
| 19 | tokens :- | |
| 20 | $white+ ; | |
| 21 | "#".* ; | |
| 22 | ||
| 23 | DONE { lex' TkDone } | |
| 24 | ||
| 25 | \{ { lex' TkLCurl } | |
| 26 | \} { lex' TkRCurl } | |
| 27 | \[ { lex' TkLBrac } | |
| 28 | \] { lex' TkRBrac } | |
| 29 | \; { lex' TkSemi } | |
| 30 | \+ { lex' TkPlus } | |
| 31 | \& { lex' TkAnd } | |
| 32 | ||
| 33 | \-\> { lex' TkArrow } | |
| 34 | ||
| 35 | $idchar + { lex (TkText . T.strip) } | |
| 36 | ||
| 37 | \$ $idchar + { lex (TkJoin . T.strip) } | |
| 38 | ||
| 39 | { | |
| 40 | data Token = Token AlexPosn TkType deriving (Eq, Show) | |
| 41 | ||
| 42 | data TkType | |
| 43 | = TkLCurl | |
| 44 | | TkRCurl | |
| 45 | | TkLBrac | |
| 46 | | TkRBrac | |
| 47 | | TkArrow | |
| 48 | | TkAnd | |
| 49 | | TkSemi | |
| 50 | | TkDone | |
| 51 | | TkPlus | |
| 52 | | TkText Text | |
| 53 | | TkJoin Text | |
| 54 | | TkEOF | |
| 55 | deriving (Eq, Show) | |
| 56 | ||
| 57 | data AlexUserState = AlexUserState | |
| 58 | { filePath :: FilePath | |
| 59 | } deriving (Eq, Show) | |
| 60 | ||
| 61 | alexInitUserState :: AlexUserState | |
| 62 | alexInitUserState = AlexUserState "<unknown>" | |
| 63 | ||
| 64 | getFilePath :: Alex FilePath | |
| 65 | getFilePath = liftM filePath alexGetUserState | |
| 66 | ||
| 67 | setFilePath :: FilePath -> Alex () | |
| 68 | setFilePath = alexSetUserState . AlexUserState | |
| 69 | ||
| 70 | alexMonadScan' :: Alex Token | |
| 71 | alexMonadScan' = do | |
| 72 | inp <- alexGetInput | |
| 73 | sc <- alexGetStartCode | |
| 74 | case alexScan inp sc of | |
| 75 | AlexEOF -> alexEOF | |
| 76 | AlexError (p, _, _, s) -> | |
| 77 | alexError' p ("lexical error at character '" ++ take 1 s ++ "'") | |
| 78 | AlexSkip inp' len -> do | |
| 79 | alexSetInput inp' | |
| 80 | alexMonadScan' | |
| 81 | AlexToken inp' len action -> do | |
| 82 | alexSetInput inp' | |
| 83 | action (ignorePendingBytes inp) len | |
| 84 | ||
| 85 | alexError' :: AlexPosn -> String -> Alex a | |
| 86 | alexError' (AlexPn _ l c) msg = do | |
| 87 | fp <- getFilePath | |
| 88 | alexError (fp ++ ":" ++ show l ++ ":" ++ show c ++ ": " ++ msg) | |
| 89 | ||
| 90 | alexEOF :: Alex Token | |
| 91 | alexEOF = do | |
| 92 | (p,_,_,_) <- alexGetInput | |
| 93 | return (Token p TkEOF) | |
| 94 | ||
| 95 | ||
| 96 | lex :: (Text -> TkType) -> AlexAction Token | |
| 97 | lex f = \(p,_,_,s) i -> return (Token p (f (T.pack (take i s)))) | |
| 98 | ||
| 99 | lex' :: TkType -> AlexAction Token | |
| 100 | lex' = lex . const | |
| 101 | ||
| 102 | runAlex' :: Alex a -> FilePath -> String -> Either String a | |
| 103 | runAlex' mote fp input = runAlex input (setFilePath fp >> mote) | |
| 104 | } | 
| 1 | { | |
| 2 | {-# LANGUAGE OverloadedStrings #-} | |
| 3 | ||
| 4 | module Apicius.Parser where | |
| 5 | ||
| 6 | import Apicius.AST | |
| 7 | import Apicius.Lexer | |
| 8 | ||
| 9 | } | |
| 10 | ||
| 11 | %name parse | |
| 12 | %tokentype { Token } | |
| 13 | %monad { Alex } | |
| 14 | %lexer { lexwrap } { Token _ TkEOF } | |
| 15 | %error { happyError } | |
| 16 | ||
| 17 | %token | |
| 18 | '{' { Token _ TkLCurl } | |
| 19 | '}' { Token _ TkRCurl } | |
| 20 | '[' { Token _ TkLBrac } | |
| 21 | ']' { Token _ TkRBrac } | |
| 22 | ';' { Token _ TkSemi } | |
| 23 | '+' { Token _ TkPlus } | |
| 24 | '&' { Token _ TkAnd } | |
| 25 | ||
| 26 | '->' { Token _ TkArrow } | |
| 27 | done { Token _ TkDone } | |
| 28 | ||
| 29 | text { Token _ (TkText $$) } | |
| 30 | join { Token _ (TkJoin $$) } | |
| 31 | ||
| 32 | %% | |
| 33 | ||
| 34 | file | |
| 35 | : recipe file { $1 : $2 } | |
| 36 | | { [] } | |
| 37 | ||
| 38 | recipe | |
| 39 | : text '{' steps '}' { Recipe $1 $3 } | |
| 40 | ||
| 41 | steps | |
| 42 | : step ';' steps { $1 : $3 } | |
| 43 | | { [] } | |
| 44 | ||
| 45 | step | |
| 46 | : input '->' actions { Step $1 $3 } | |
| 47 | ||
| 48 | input | |
| 49 | : ilist { InpIngredients $1 } | |
| 50 | | join { InpJoin $1 } | |
| 51 | ||
| 52 | ilist | |
| 53 | : ingredients { IngredientList $1 } | |
| 54 | ||
| 55 | ingredients | |
| 56 | : ingredient '+' ingredients { $1 : $3 } | |
| 57 | | ingredient { [$1] } | |
| 58 | ||
| 59 | ingredient | |
| 60 | : '[' text ']' text { Ingredient (Just $2) $4 } | |
| 61 | | text { Ingredient Nothing $1 } | |
| 62 | ||
| 63 | actions | |
| 64 | : action '->' actions { $1 : $3 } | |
| 65 | | action { [$1] } | |
| 66 | ||
| 67 | action | |
| 68 | : text { Action $1 Nothing } | |
| 69 | | text '&' ilist { Action $1 (Just $3) } | |
| 70 | | join { Join $1 } | |
| 71 | | done { Done } | |
| 72 | ||
| 73 | { | |
| 74 | ||
| 75 | lexwrap :: (Token -> Alex a) -> Alex a | |
| 76 | lexwrap = (alexMonadScan' >>=) | |
| 77 | ||
| 78 | happyError :: Token -> Alex a | |
| 79 | happyError (Token p t) = | |
| 80 | alexError' p ("parse error at token " ++ show t) | |
| 81 | ||
| 82 | parseFile :: FilePath -> String -> Either String [Recipe] | |
| 83 | parseFile = runAlex' parse | |
| 84 | } | 
| 1 | {-# LANGUAGE ParallelListComp #-} | |
| 2 | ||
| 3 | module Apicius.Render.Dot (showDotGraph) where | |
| 4 | ||
| 5 | import Data.Monoid ((<>)) | |
| 6 | import Data.Text (Text) | |
| 7 | import qualified Data.Text as T | |
| 8 | ||
| 9 | import Apicius.Language | |
| 10 | import Apicius.ReverseTree | |
| 11 | import Apicius.Util | |
| 12 | ||
| 13 | showDotGraph :: Recipe -> Text | |
| 14 | showDotGraph r@(Recipe name _) = | |
| 15 | dotGraph name . buildReverseGraph . getChunks $ r | |
| 16 | ||
| 17 | dotGraph :: Text -> ReverseGraph -> Text | |
| 18 | dotGraph rname gr = | |
| 19 | ("digraph \"" <> rname <> "\" {\n") <> T.unlines (go "n" 0 gr) <> "\n}" | |
| 20 | where go :: Text -> Int -> ReverseGraph -> [Text] | |
| 21 | go parent n (ReverseGraph t rs) = | |
| 22 | let name = parent <> "_" <> text n | |
| 23 | children = [ (i, name <> "_" <> text i, r) | |
| 24 | | i <- [0..] | |
| 25 | | r <- rs | |
| 26 | ] | |
| 27 | in [ " " <> name <> stepMeta t ] ++ | |
| 28 | [ " " <> cname <> " -> " <> name <> ";" | |
| 29 | | (_, cname, _) <- children | |
| 30 | ] ++ | |
| 31 | concat [ go name i r | |
| 32 | | (i, _, r) <- children | |
| 33 | ] | |
| 34 | ||
| 35 | stepMeta :: Either IngredientList Text -> Text | |
| 36 | stepMeta (Right t) = " [label=\"" <> t <> "\",color=red]" | |
| 37 | stepMeta (Left (IngredientList is)) = | |
| 38 | " [label=\"" <> T.intercalate "; " [ ingName i | i <- is ] <> "\"]" | 
| 1 | {-# LANGUAGE ParallelListComp #-} | |
| 2 | ||
| 3 | module Apicius.ReverseTree where | |
| 4 | ||
| 5 | import Data.Monoid ((<>)) | |
| 6 | import qualified Data.Text as T | |
| 7 | import Data.Text (Text) | |
| 8 | ||
| 9 | import Apicius.AST | |
| 10 | ||
| 11 | -- An 'ActionChunk' represents a set of actions in between two join | |
| 12 | -- points. This is 'reversed' from what we'd expect: the 'name' is | |
| 13 | -- actually the name of the join point at the end of a sequence of | |
| 14 | -- actions, or the string DONE, while the 'prev' is the name of the | |
| 15 | -- join point that came at the beginning, or the ingredients list | |
| 16 | -- that started the rule. The actions also will appear in reverse | |
| 17 | -- order. | |
| 18 | ||
| 19 | -- Maybe an explanation is in order: this rule | |
| 20 | -- ingredients -> a -> $x -> b -> c -> $y; | |
| 21 | -- will produce two ActionChunks: | |
| 22 | -- ActionChunk $y [c, b] (Right $x) | |
| 23 | -- and | |
| 24 | -- ActionChunk $x [a] (Left ingredients) | |
| 25 | data ActionChunk = ActionChunk | |
| 26 | { acName :: Text | |
| 27 | , acRules :: [Text] | |
| 28 | , acPrev :: Either IngredientList Text | |
| 29 | } deriving (Eq, Show) | |
| 30 | ||
| 31 | -- This is the function that actually splits apart the action into | |
| 32 | -- ActionChunks. It's grosser than I'd hoped, but it's mostly a lot | |
| 33 | -- of fiddly but straightforward traversing. | |
| 34 | splitApart :: Either IngredientList Text -> [Action] -> [ActionChunk] | |
| 35 | splitApart i = toChunk [] . reverse | |
| 36 | where toChunk cs (Join t:xs) = | |
| 37 | gather t xs [] cs | |
| 38 | toChunk cs (Action "DONE" _:xs) = | |
| 39 | gather "DONE" xs [] cs | |
| 40 | toChunk cs (Done:xs) = | |
| 41 | gather "DONE" xs [] cs | |
| 42 | toChunk _ (Action _ _:_) = | |
| 43 | error "expected chunk to end with a join or DONE" | |
| 44 | toChunk cs [] = cs | |
| 45 | gather n xs@(Join t:_) as cs = | |
| 46 | toChunk (ActionChunk n (reverse as) (Right t) : cs) xs | |
| 47 | gather n (Action t _:xs) as cs = | |
| 48 | gather n xs (t:as) cs | |
| 49 | gather _ (Done:_) _ _ = | |
| 50 | error "unsure how to handle this case" | |
| 51 | gather n [] as cs = | |
| 52 | ActionChunk n (reverse as) i : cs | |
| 53 | ||
| 54 | -- Here we take a recipe and pull all the ActionChunks into a single | |
| 55 | -- list. | |
| 56 | getChunks :: Recipe -> [ActionChunk] | |
| 57 | getChunks Recipe { rRecipe = st } = | |
| 58 | mconcat (map getActions st) | |
| 59 | where getActions (Step (InpJoin t) as) = splitApart (Right t) as | |
| 60 | getActions (Step (InpIngredients is) as) = splitApart (Left is) as | |
| 61 | ||
| 62 | -- The ReverseGraph is a tree rooted at the DONE node. The 'children' | |
| 63 | -- are actually the steps leading up to a given node. Only childless | |
| 64 | -- nodes should have an IngredientList associated with them, but we | |
| 65 | -- don't encode this invariant in the type. | |
| 66 | data ReverseGraph = ReverseGraph | |
| 67 | { rStep :: Either IngredientList Text | |
| 68 | , rPrevs :: [ReverseGraph] | |
| 69 | } deriving (Eq, Show) | |
| 70 | ||
| 71 | -- Take a list of ActionChunks and stitch them back together so that | |
| 72 | -- we can build a ReverseGraph of them. Again, fiddly but straightforward | |
| 73 | -- traversing of the data structures. | |
| 74 | buildReverseGraph :: [ActionChunk] -> ReverseGraph | |
| 75 | buildReverseGraph as = ReverseGraph (Right "DONE") | |
| 76 | (concat (map buildFrom (findChunks "DONE"))) | |
| 77 | where findChunks n = [ chunk | chunk <- as, acName chunk == n ] | |
| 78 | buildFrom (ActionChunk _ rs p) = go rs p | |
| 79 | go [] (Right p) = concat $ map buildFrom (findChunks p) | |
| 80 | go [] (Left i) = [ReverseGraph (Left i) []] | |
| 81 | go (r:rs) p = [ReverseGraph (Right r) (go rs p)] | |
| 82 | ||
| 83 | -- Prettily convert a ReverseGraph to a readable tree. This will give | |
| 84 | -- us a recipe tree in reverse order, starting with the DONE, and | |
| 85 | -- gradually going back to the ingredients. | |
| 86 | prettyGraph :: ReverseGraph -> Text | |
| 87 | prettyGraph = go 0 | |
| 88 | where go n (ReverseGraph t rs) = | |
| 89 | indent n <> stepName t <> "\n" <> T.concat (map (go (n+2)) rs) | |
| 90 | indent n = T.replicate n " " | |
| 91 | ||
| 92 | stepName :: Either IngredientList Text -> Text | |
| 93 | stepName (Right t) = t | |
| 94 | stepName (Left (IngredientList is)) = | |
| 95 | T.intercalate "; " [ ingName i | i <- is ] | |
| 96 | ||
| 97 | ingName :: Ingredient -> Text | |
| 98 | ingName (Ingredient (Just amt) name) = amt <> " " <> name | |
| 99 | ingName (Ingredient Nothing name) = name | |
| 100 | ||
| 101 | showFragments :: Recipe -> Text | |
| 102 | showFragments = T.pack . show . getChunks | |
| 103 | ||
| 104 | showReverseTree :: Recipe -> Text | |
| 105 | showReverseTree = prettyGraph . buildReverseGraph . getChunks | 
| 1 | {-# LANGUAGE DefaultSignatures #-} | |
| 2 | ||
| 3 | module Apicius.Util where | |
| 4 | ||
| 5 | import Data.Monoid ((<>)) | |
| 6 | import Data.Text.Buildable (Buildable(..)) | |
| 7 | import Data.Text (Text) | |
| 8 | import qualified Data.Text as T | |
| 9 | import Data.Text.Lazy (toStrict) | |
| 10 | import Data.Text.Lazy.Builder (toLazyText) | |
| 11 | ||
| 12 | class TShow s where | |
| 13 | text :: s -> Text | |
| 14 | ||
| 15 | default text :: Buildable s => s -> Text | |
| 16 | text = toStrict . toLazyText . build | |
| 17 | ||
| 18 | instance TShow Int where | |
| 19 | instance TShow Text where | |
| 20 | ||
| 21 | instance TShow a => TShow [a] where | |
| 22 | text xs = "[" <> T.intercalate ", " (map text xs) <> "]" | |
| 23 | ||
| 24 | instance TShow a => TShow (Maybe a) where | |
| 25 | text Nothing = "Nothing" | |
| 26 | text (Just a) = "Just " <> text a | 
| 11 | 11 | build-type: Simple | 
| 12 | 12 | cabal-version: >= 1.12 | 
| 13 | 13 | |
| 14 | executable apicius | |
| 15 | hs-source-dirs: src | |
| 16 | main-is: Main.hs | |
| 17 | other-modules: Lexer, | |
| 18 | Parser, | |
| 19 | AST, | |
| 20 | BuildTree, | |
| 21 | Util | |
| 14 | library | |
| 15 | exposed-modules: Apicius.Language, | |
| 16 | Apicius.ReverseTree, | |
| 17 | Apicius.Render.Dot | |
| 18 | other-modules: Apicius.AST, | |
| 19 | Apicius.Lexer, | |
| 20 | Apicius.Parser, | |
| 21 | Apicius.Util | |
| 22 | build-depends: base >=4.7 && <4.9, | |
| 23 | text, | |
| 24 | text-format, | |
| 25 | array | |
| 22 | 26 | build-tools: happy, alex | 
| 23 | 27 | default-extensions: OverloadedStrings, | 
| 24 | 28 | ScopedTypeVariables | 
| 25 | 29 | ghc-options: -Wall | 
| 30 | default-language: Haskell2010 | |
| 31 | ||
| 32 | executable apicius | |
| 33 | hs-source-dirs: src | |
| 34 | main-is: Main.hs | |
| 35 | default-extensions: OverloadedStrings, | |
| 36 | ScopedTypeVariables | |
| 37 | ghc-options: -Wall | |
| 26 | 38 | build-depends: base >=4.7 && <4.9, | 
| 39 | apicius, | |
| 27 | 40 | text, | 
| 28 | text-format, | |
| 29 | array, | |
| 30 | 41 | errors | 
| 31 | 42 | default-language: Haskell2010 | 
| 1 | module AST where | |
| 2 | ||
| 3 | import Data.Monoid ((<>)) | |
| 4 | import Data.Text (Text) | |
| 5 | import Util | |
| 6 | ||
| 7 | data Recipe = Recipe | |
| 8 | { rName :: Text | |
| 9 | , rRecipe :: [Step] | |
| 10 | } deriving (Eq, Show) | |
| 11 | ||
| 12 | instance TShow Recipe where | |
| 13 | text (Recipe name steps) = | |
| 14 | "Recipe { rName = " <> | |
| 15 | text name <> | |
| 16 | ", rRecipe = " <> | |
| 17 | text steps <> | |
| 18 | " }" | |
| 19 | ||
| 20 | data Step = Step | |
| 21 | { sInputs :: Input | |
| 22 | , sActions :: [Action] | |
| 23 | } deriving (Eq, Show) | |
| 24 | ||
| 25 | instance TShow Step where | |
| 26 | text (Step inp acts) = | |
| 27 | "Step { sInputs = " <> | |
| 28 | text inp <> | |
| 29 | ", sActions = " <> | |
| 30 | text acts <> | |
| 31 | " }" | |
| 32 | ||
| 33 | data Input | |
| 34 | = InpIngredients IngredientList | |
| 35 | | InpJoin Text | |
| 36 | deriving (Eq, Show) | |
| 37 | ||
| 38 | instance TShow Input where | |
| 39 | text (InpJoin ts) = text ts | |
| 40 | text (InpIngredients is) = text is | |
| 41 | ||
| 42 | data IngredientList = IngredientList | |
| 43 | { fromIngredientList :: [Ingredient] | |
| 44 | } deriving (Eq, Show) | |
| 45 | ||
| 46 | instance TShow IngredientList where | |
| 47 | text (IngredientList is) = | |
| 48 | "IngredientList " <> text is | |
| 49 | ||
| 50 | data Ingredient = Ingredient | |
| 51 | { iAmount :: Maybe Text | |
| 52 | , iType :: Text | |
| 53 | } deriving (Eq, Show) | |
| 54 | ||
| 55 | instance TShow Ingredient where | |
| 56 | text (Ingredient i t) = | |
| 57 | "Ingredient { iAmount = " <> | |
| 58 | text i <> | |
| 59 | ", iType = " <> | |
| 60 | text t <> | |
| 61 | " }" | |
| 62 | ||
| 63 | data Action | |
| 64 | = Action Text (Maybe IngredientList) | |
| 65 | | Join Text | |
| 66 | | Done | |
| 67 | deriving (Eq, Show) | |
| 68 | ||
| 69 | instance TShow Action where | |
| 70 | text (Action t i) = "Action " <> text t <> " " <> text i | |
| 71 | text (Join t) = "Join " <> text t | |
| 72 | text Done = "Done" | |
| 73 | 
| 1 | {-# LANGUAGE ParallelListComp #-} | |
| 2 | ||
| 3 | module BuildTree where | |
| 4 | ||
| 5 | import Data.Monoid ((<>)) | |
| 6 | import qualified Data.Text as T | |
| 7 | import Data.Text (Text) | |
| 8 | ||
| 9 | import AST | |
| 10 | import Util | |
| 11 | ||
| 12 | -- An 'ActionChunk' represents a set of actions in between two join | |
| 13 | -- points. This is 'reversed' from what we'd expect: the 'name' is | |
| 14 | -- actually the name of the join point at the end of a sequence of | |
| 15 | -- actions, or the string DONE, while the 'prev' is the name of the | |
| 16 | -- join point that came at the beginning, or the ingredients list | |
| 17 | -- that started the rule. The actions also will appear in reverse | |
| 18 | -- order. | |
| 19 | ||
| 20 | -- Maybe an explanation is in order: this rule | |
| 21 | -- ingredients -> a -> $x -> b -> c -> $y; | |
| 22 | -- will produce two ActionChunks: | |
| 23 | -- ActionChunk $y [c, b] (Right $x) | |
| 24 | -- and | |
| 25 | -- ActionChunk $x [a] (Left ingredients) | |
| 26 | data ActionChunk = ActionChunk | |
| 27 | { acName :: Text | |
| 28 | , acRules :: [Text] | |
| 29 | , acPrev :: Either IngredientList Text | |
| 30 | } deriving (Eq, Show) | |
| 31 | ||
| 32 | -- This is the function that actually splits apart the action into | |
| 33 | -- ActionChunks. It's grosser than I'd hoped, but it's mostly a lot | |
| 34 | -- of fiddly but straightforward traversing. | |
| 35 | splitApart :: Either IngredientList Text -> [Action] -> [ActionChunk] | |
| 36 | splitApart i = toChunk [] . reverse | |
| 37 | where toChunk cs (Join t:xs) = | |
| 38 | gather t xs [] cs | |
| 39 | toChunk cs (Action "DONE" _:xs) = | |
| 40 | gather "DONE" xs [] cs | |
| 41 | toChunk cs (Done:xs) = | |
| 42 | gather "DONE" xs [] cs | |
| 43 | toChunk _ (Action _ _:_) = | |
| 44 | error "expected chunk to end with a join or DONE" | |
| 45 | toChunk cs [] = cs | |
| 46 | gather n xs@(Join t:_) as cs = | |
| 47 | toChunk (ActionChunk n (reverse as) (Right t) : cs) xs | |
| 48 | gather n (Action t _:xs) as cs = | |
| 49 | gather n xs (t:as) cs | |
| 50 | gather _ (Done:_) _ _ = | |
| 51 | error "unsure how to handle this case" | |
| 52 | gather n [] as cs = | |
| 53 | ActionChunk n (reverse as) i : cs | |
| 54 | ||
| 55 | -- Here we take a recipe and pull all the ActionChunks into a single | |
| 56 | -- list. | |
| 57 | getChunks :: Recipe -> [ActionChunk] | |
| 58 | getChunks Recipe { rRecipe = st } = | |
| 59 | mconcat (map getActions st) | |
| 60 | where getActions (Step (InpJoin t) as) = splitApart (Right t) as | |
| 61 | getActions (Step (InpIngredients is) as) = splitApart (Left is) as | |
| 62 | ||
| 63 | -- The ReverseGraph is a tree rooted at the DONE node. The 'children' | |
| 64 | -- are actually the steps leading up to a given node. Only childless | |
| 65 | -- nodes should have an IngredientList associated with them, but we | |
| 66 | -- don't encode this invariant in the type. | |
| 67 | data ReverseGraph = ReverseGraph | |
| 68 | { rStep :: Either IngredientList Text | |
| 69 | , rPrevs :: [ReverseGraph] | |
| 70 | } deriving (Eq, Show) | |
| 71 | ||
| 72 | -- Take a list of ActionChunks and stitch them back together so that | |
| 73 | -- we can build a ReverseGraph of them. Again, fiddly but straightforward | |
| 74 | -- traversing of the data structures. | |
| 75 | buildReverseGraph :: [ActionChunk] -> ReverseGraph | |
| 76 | buildReverseGraph as = ReverseGraph (Right "DONE") | |
| 77 | (concat (map buildFrom (findChunks "DONE"))) | |
| 78 | where findChunks n = [ chunk | chunk <- as, acName chunk == n ] | |
| 79 | buildFrom (ActionChunk _ rs p) = go rs p | |
| 80 | go [] (Right p) = concat $ map buildFrom (findChunks p) | |
| 81 | go [] (Left i) = [ReverseGraph (Left i) []] | |
| 82 | go (r:rs) p = [ReverseGraph (Right r) (go rs p)] | |
| 83 | ||
| 84 | -- Prettily convert a ReverseGraph to a readable tree. This will give | |
| 85 | -- us a recipe tree in reverse order, starting with the DONE, and | |
| 86 | -- gradually going back to the ingredients. | |
| 87 | prettyGraph :: ReverseGraph -> Text | |
| 88 | prettyGraph = go 0 | |
| 89 | where go n (ReverseGraph t rs) = | |
| 90 | indent n <> stepName t <> "\n" <> T.concat (map (go (n+2)) rs) | |
| 91 | indent n = T.replicate n " " | |
| 92 | ||
| 93 | stepName :: Either IngredientList Text -> Text | |
| 94 | stepName (Right t) = t | |
| 95 | stepName (Left (IngredientList is)) = | |
| 96 | T.intercalate "; " [ ingName i | i <- is ] | |
| 97 | ||
| 98 | ingName :: Ingredient -> Text | |
| 99 | ingName (Ingredient (Just amt) name) = amt <> " " <> name | |
| 100 | ingName (Ingredient Nothing name) = name | |
| 101 | ||
| 102 | stepMeta :: Either IngredientList Text -> Text | |
| 103 | stepMeta (Right t) = " [label=\"" <> t <> "\",color=red]" | |
| 104 | stepMeta (Left (IngredientList is)) = | |
| 105 | " [label=\"" <> T.intercalate "; " [ ingName i | i <- is ] <> "\"]" | |
| 106 | ||
| 107 | dotGraph :: Text -> ReverseGraph -> Text | |
| 108 | dotGraph rname gr = | |
| 109 | ("digraph \"" <> rname <> "\" {\n") <> T.unlines (go "n" 0 gr) <> "\n}" | |
| 110 | where go :: Text -> Int -> ReverseGraph -> [Text] | |
| 111 | go parent n (ReverseGraph t rs) = | |
| 112 | let name = parent <> "_" <> text n | |
| 113 | children = [ (i, name <> "_" <> text i, r) | |
| 114 | | i <- [0..] | |
| 115 | | r <- rs | |
| 116 | ] | |
| 117 | in [ " " <> name <> stepMeta t ] ++ | |
| 118 | [ " " <> cname <> " -> " <> name <> ";" | |
| 119 | | (_, cname, _) <- children | |
| 120 | ] ++ | |
| 121 | concat [ go name i r | |
| 122 | | (i, _, r) <- children | |
| 123 | ] | |
| 124 | ||
| 125 | showFragments :: Recipe -> Text | |
| 126 | showFragments = T.pack . show . getChunks | |
| 127 | ||
| 128 | showTree :: Recipe -> Text | |
| 129 | showTree = prettyGraph . buildReverseGraph . getChunks | |
| 130 | ||
| 131 | showDotGraph :: Recipe -> Text | |
| 132 | showDotGraph r@(Recipe name _) = | |
| 133 | dotGraph name . buildReverseGraph . getChunks $ r | 
| 1 | { | |
| 2 | {-# OPTIONS -w #-} | |
| 3 | {-# LANGUAGE OverloadedStrings #-} | |
| 4 | ||
| 5 | module Lexer where | |
| 6 | ||
| 7 | import Control.Monad (liftM) | |
| 8 | import Data.Text (Text) | |
| 9 | import qualified Data.Text as T | |
| 10 | ||
| 11 | import Prelude hiding (lex) | |
| 12 | } | |
| 13 | ||
| 14 | %wrapper "monadUserState" | |
| 15 | ||
| 16 | $special = [ \{ \} \[ \] \( \) \; \, \+ \& \- \$ ] | |
| 17 | $idchar = $printable # $special | |
| 18 | ||
| 19 | tokens :- | |
| 20 | $white+ ; | |
| 21 | "#".* ; | |
| 22 | ||
| 23 | DONE { lex' TkDone } | |
| 24 | ||
| 25 | \{ { lex' TkLCurl } | |
| 26 | \} { lex' TkRCurl } | |
| 27 | \[ { lex' TkLBrac } | |
| 28 | \] { lex' TkRBrac } | |
| 29 | \( { lex' TkLParn } | |
| 30 | \) { lex' TkRParn } | |
| 31 | \; { lex' TkSemi } | |
| 32 | \, { lex' TkComma } | |
| 33 | \+ { lex' TkPlus } | |
| 34 | \& { lex' TkAnd } | |
| 35 | ||
| 36 | \-\> { lex' TkArrow } | |
| 37 | ||
| 38 | $idchar + { lex (TkText . T.strip) } | |
| 39 | ||
| 40 | \$ $idchar + { lex (TkJoin . T.strip) } | |
| 41 | ||
| 42 | { | |
| 43 | data Token = Token AlexPosn TkType deriving (Eq, Show) | |
| 44 | ||
| 45 | data TkType | |
| 46 | = TkLCurl | |
| 47 | | TkRCurl | |
| 48 | | TkLBrac | |
| 49 | | TkRBrac | |
| 50 | | TkLParn | |
| 51 | | TkRParn | |
| 52 | | TkArrow | |
| 53 | | TkComma | |
| 54 | | TkAnd | |
| 55 | | TkSemi | |
| 56 | | TkDone | |
| 57 | | TkPlus | |
| 58 | | TkText Text | |
| 59 | | TkJoin Text | |
| 60 | | TkEOF | |
| 61 | deriving (Eq, Show) | |
| 62 | ||
| 63 | data AlexUserState = AlexUserState | |
| 64 | { filePath :: FilePath | |
| 65 | } deriving (Eq, Show) | |
| 66 | ||
| 67 | alexInitUserState :: AlexUserState | |
| 68 | alexInitUserState = AlexUserState "<unknown>" | |
| 69 | ||
| 70 | getFilePath :: Alex FilePath | |
| 71 | getFilePath = liftM filePath alexGetUserState | |
| 72 | ||
| 73 | setFilePath :: FilePath -> Alex () | |
| 74 | setFilePath = alexSetUserState . AlexUserState | |
| 75 | ||
| 76 | alexMonadScan' :: Alex Token | |
| 77 | alexMonadScan' = do | |
| 78 | inp <- alexGetInput | |
| 79 | sc <- alexGetStartCode | |
| 80 | case alexScan inp sc of | |
| 81 | AlexEOF -> alexEOF | |
| 82 | AlexError (p, _, _, s) -> | |
| 83 | alexError' p ("lexical error at character '" ++ take 1 s ++ "'") | |
| 84 | AlexSkip inp' len -> do | |
| 85 | alexSetInput inp' | |
| 86 | alexMonadScan' | |
| 87 | AlexToken inp' len action -> do | |
| 88 | alexSetInput inp' | |
| 89 | action (ignorePendingBytes inp) len | |
| 90 | ||
| 91 | alexError' :: AlexPosn -> String -> Alex a | |
| 92 | alexError' (AlexPn _ l c) msg = do | |
| 93 | fp <- getFilePath | |
| 94 | alexError (fp ++ ":" ++ show l ++ ":" ++ show c ++ ": " ++ msg) | |
| 95 | ||
| 96 | alexEOF :: Alex Token | |
| 97 | alexEOF = do | |
| 98 | (p,_,_,_) <- alexGetInput | |
| 99 | return (Token p TkEOF) | |
| 100 | ||
| 101 | ||
| 102 | lex :: (Text -> TkType) -> AlexAction Token | |
| 103 | lex f = \(p,_,_,s) i -> return (Token p (f (T.pack (take i s)))) | |
| 104 | ||
| 105 | lex' :: TkType -> AlexAction Token | |
| 106 | lex' = lex . const | |
| 107 | ||
| 108 | runAlex' :: Alex a -> FilePath -> String -> Either String a | |
| 109 | runAlex' mote fp input = runAlex input (setFilePath fp >> mote) | |
| 110 | } | 
| 10 | 10 | , stdout | 
| 11 | 11 | ) | 
| 12 | 12 | |
| 13 | import AST (Recipe) | |
| 14 | import BuildTree (showFragments, showDotGraph, showTree) | |
| 15 | import Parser (parseFile) | |
| 16 | import Util (TShow(text)) | |
| 13 | import Apicius.ReverseTree (showFragments, showReverseTree) | |
| 14 | import Apicius.Render.Dot (showDotGraph) | |
| 15 | import Apicius.Language (Recipe, parseFile, showAst) | |
| 17 | 16 | |
| 18 | 17 | usage :: String | 
| 19 | 18 | usage = | 
| 21 | 20 | |
| 22 | 21 | renderers :: [(String, Recipe -> Text)] | 
| 23 | 22 | renderers = | 
| 24 | [ ("ast", text) | |
| 25 | , ("fragments", showFragments) | |
| 26 | , ("dot", showDotGraph) | |
| 27 | , ("reverse-tree", showTree) | |
| 23 | [ ("ast", showAst) | |
| 24 | , ("fragments", showFragments) | |
| 25 | , ("dot", showDotGraph) | |
| 26 | , ("reverse-tree", showReverseTree) | |
| 28 | 27 | ] | 
| 29 | 28 | |
| 30 | 29 | main :: IO () | 
| 1 | { | |
| 2 | {-# LANGUAGE OverloadedStrings #-} | |
| 3 | ||
| 4 | module Parser where | |
| 5 | ||
| 6 | import AST | |
| 7 | import Lexer | |
| 8 | ||
| 9 | } | |
| 10 | ||
| 11 | %name parse | |
| 12 | %tokentype { Token } | |
| 13 | %monad { Alex } | |
| 14 | %lexer { lexwrap } { Token _ TkEOF } | |
| 15 | %error { happyError } | |
| 16 | ||
| 17 | %token | |
| 18 | '{' { Token _ TkLCurl } | |
| 19 | '}' { Token _ TkRCurl } | |
| 20 | '[' { Token _ TkLBrac } | |
| 21 | ']' { Token _ TkRBrac } | |
| 22 | '(' { Token _ TkLParn } | |
| 23 | ')' { Token _ TkRParn } | |
| 24 | ';' { Token _ TkSemi } | |
| 25 | ',' { Token _ TkComma } | |
| 26 | '+' { Token _ TkPlus } | |
| 27 | '&' { Token _ TkAnd } | |
| 28 | ||
| 29 | '->' { Token _ TkArrow } | |
| 30 | done { Token _ TkDone } | |
| 31 | ||
| 32 | text { Token _ (TkText $$) } | |
| 33 | join { Token _ (TkJoin $$) } | |
| 34 | ||
| 35 | %% | |
| 36 | ||
| 37 | file | |
| 38 | : recipe file { $1 : $2 } | |
| 39 | | { [] } | |
| 40 | ||
| 41 | recipe | |
| 42 | : text '{' steps '}' { Recipe $1 $3 } | |
| 43 | ||
| 44 | steps | |
| 45 | : step ';' steps { $1 : $3 } | |
| 46 | | { [] } | |
| 47 | ||
| 48 | step | |
| 49 | : input '->' actions { Step $1 $3 } | |
| 50 | ||
| 51 | input | |
| 52 | : ilist { InpIngredients $1 } | |
| 53 | | join { InpJoin $1 } | |
| 54 | ||
| 55 | ilist | |
| 56 | : ingredients { IngredientList $1 } | |
| 57 | ||
| 58 | ingredients | |
| 59 | : ingredient '+' ingredients { $1 : $3 } | |
| 60 | | ingredient { [$1] } | |
| 61 | ||
| 62 | ingredient | |
| 63 | : '[' text ']' text { Ingredient (Just $2) $4 } | |
| 64 | | text { Ingredient Nothing $1 } | |
| 65 | ||
| 66 | actions | |
| 67 | : action '->' actions { $1 : $3 } | |
| 68 | | action { [$1] } | |
| 69 | ||
| 70 | action | |
| 71 | : text { Action $1 Nothing } | |
| 72 | | text '&' ilist { Action $1 (Just $3) } | |
| 73 | | join { Join $1 } | |
| 74 | | done { Done } | |
| 75 | ||
| 76 | { | |
| 77 | ||
| 78 | lexwrap :: (Token -> Alex a) -> Alex a | |
| 79 | lexwrap = (alexMonadScan' >>=) | |
| 80 | ||
| 81 | happyError :: Token -> Alex a | |
| 82 | happyError (Token p t) = | |
| 83 | alexError' p ("parse error at token " ++ show t) | |
| 84 | ||
| 85 | parseFile :: FilePath -> String -> Either String [Recipe] | |
| 86 | parseFile = runAlex' parse | |
| 87 | } | 
| 1 | {-# LANGUAGE DefaultSignatures #-} | |
| 2 | ||
| 3 | module Util where | |
| 4 | ||
| 5 | import Data.Monoid ((<>)) | |
| 6 | import Data.Text.Buildable (Buildable(..)) | |
| 7 | import Data.Text (Text) | |
| 8 | import qualified Data.Text as T | |
| 9 | import Data.Text.Lazy (toStrict) | |
| 10 | import Data.Text.Lazy.Builder (toLazyText) | |
| 11 | ||
| 12 | class TShow s where | |
| 13 | text :: s -> Text | |
| 14 | ||
| 15 | default text :: Buildable s => s -> Text | |
| 16 | text = toStrict . toLazyText . build | |
| 17 | ||
| 18 | instance TShow Int where | |
| 19 | instance TShow Text where | |
| 20 | ||
| 21 | instance TShow a => TShow [a] where | |
| 22 | text xs = "[" <> T.intercalate ", " (map text xs) <> "]" | |
| 23 | ||
| 24 | instance TShow a => TShow (Maybe a) where | |
| 25 | text Nothing = "Nothing" | |
| 26 | text (Just a) = "Just " <> text a |