packages feed

joy-rewrite-0.1.0.0: src/Language/Joy/Rewrite.hs

{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-# LANGUAGE DeriveFunctor, FlexibleContexts, LambdaCase, OverloadedStrings #-}
-- |
-- Maintainer  :  Johannes Riecken <johannes.riecken@gmail.com>
-- Stability   :  experimental
-- Portability :  non-portable
--
-- This module includes combinators for rewriting Joy source code.
----------------------------------------------------------------------------
module Language.Joy.Rewrite
  ( rewrite
  , tokenize
  ) where
import Control.Applicative
import Control.Arrow
import Control.Monad
import Control.Monad.Loops
import Data.Foldable (asum)
import Data.List
import qualified Data.Text as T
import Data.Text (Text(..))
import qualified Data.Map as M
import Data.Map (Map)
import Test.Hspec
import Text.Parsec hiding ((<|>), many)

-- | Error is a generic error type.
type Error = Text

-- A rewrite rule matches a pattern and tries to rewrite it with the
-- replacement. If there is a condition attached, the rewriting will only happen
-- if the rewrite given in the condition can happen based on known rewrite
-- rules.
data Rule = Rule
  { pat       :: [RuleExpr Text]
  , repl      :: [RuleExpr Text]
  , condition :: Maybe Condition
  }
  deriving Show

data Condition = Condition
  { premise    :: [RuleExpr Text]
  , conclusion :: [RuleExpr Text]
  }
  deriving Show

convertError :: Either ParseError a -> Either Error a
convertError (Left x) = Left . T.pack $ show x
convertError (Right x) = Right x

-- Construct a Rule by parsing a string like "a b swap => b a"
mkRule :: Text -> Either Error Rule
mkRule xs = do
  xs' <- tokenizeRule xs
  if CondSep `elem` xs'
    then do
      (v,w) <- cut CondSep xs'
      (a,b) <- cut RuleSep v
      (c,d) <- cut RuleSep w
      pure $ Rule a b (Just $ Condition c d)
    else do
      (a, b) <- tokenizeFromTo xs
      pure $ Rule a b Nothing where

  tokenizeFromTo :: Text -> Either Error ([RuleExpr Text], [RuleExpr Text])
  tokenizeFromTo xs = tokenizeRule xs >>= cut RuleSep

  cut :: Eq a => a -> [a] -> Either Error ([a], [a])
  cut _ [] = Left "tried to cut empty list"
  cut sep xs = Right . second tail . break (== sep) $ xs

  tokenizeRule :: Text -> Either Error [RuleExpr Text]
  tokenizeRule = convertError . fmap (fmap (fmap T.pack)) . parse parser "" . T.unpack   where
    parser =
      let metaVar     = MetaVar <$> fmap pure lower
          metaListVar = MetaListVar <$> fmap pure upper
          var = Var <$> (
                string "["
            <|> string "]"
            <|> do
                  x <- string "="
                  notFollowedBy (string ">")
                  pure x
            <|> many1 digit
            <|> ((:) <$> lower <*> many1 alphaNum)
            )
          ruleSep = RuleSep <$ string "=>"
          condSep = CondSep <$ string ":-"
      in  sepBy (try var <|> metaVar <|> metaListVar <|> ruleSep <|> condSep) spaces

-- An atom in a Rule
data RuleExpr a =
      Var a -- matches with one exact atom, e.g. "swap"
    | MetaVar a -- can match with any single atom and assign it to the variable name
    | MetaListVar a -- matches a list of atoms and assigns it to the variable name
    | RuleSep -- "=>"
    | CondSep -- ":-" (read as "if")
    deriving (Show, Eq, Ord, Functor)

-- Stores the associations between rule variables and their matched Joy code
type RuleMap a = Map (RuleExpr a) [a]

-- | Given a list of rewrite rules and Joy code, apply the rules and return the resulting
--   list of tokens.
rewrite :: [Text] -> Text -> Either Error [Text]
rewrite ruleStrs code = do
  rs <- traverse mkRule ruleStrs
  rewrite' rs (tokenize code) where

  rewrite' :: [Rule] -> [Text] -> Either Error [Text]
  rewrite' rules = fmap concat . unfoldrM
    (\case
      [] -> Right Nothing
      xxs@(x : xs) ->
        Right $ asum (map (\r -> case matchPat r xxs of
                    Right (s, m) -> do
                      case condition r of
                        Nothing -> Just (concatMap (\k -> case k of
                            Var k' -> M.findWithDefault [k'] k m
                            _      -> m M.! k
                            )
                            (repl r)
                          , drop (length s) xxs
                          )
                        Just c -> case rewrite' rules (apply m $ premise c) of
                          Right x' -> case matchConclusion r x' of
                            Right (_, m') -> Just (replTokens, drop (length s) xxs)
                              where replTokens = apply (M.union m m') $ repl r
                            _ -> Nothing
                          _ -> Nothing
                    _ -> Nothing
                  ) rules
                )
          <|> Just ([x], xs)
    )

  -- Applies the stored rewrite associations to Joy code.
  apply :: RuleMap Text -> [RuleExpr Text] -> [Text]
  apply m = (=<<) (\x -> M.findWithDefault [(\case (Var x') -> x') x] x m)

  -- Matches the pattern part of a rule. I didn't find anything like `runState`
  -- for Parsec, so I return both the value and the state in the same tuple
  -- format.
  matchPat :: Rule -> [Text] -> Either Error ([Text], RuleMap Text)
  matchPat r = convertError . runParser
    (do
      x <- mkParser (pat r)
      y <- getState
      pure (x, y)
    )
    M.empty
    ""

  matchConclusion
    :: Rule -> [Text] -> Either Error ([Text], RuleMap Text)
  matchConclusion r xs = do
    conc <- conclusion <$> maybeToParseResult (condition r)
    convertError $ runParser
      (do
        x <- mkParser conc
        y <- getState
        pure (x, y)
      )
      M.empty
      ""
      xs where
    -- I think this is also a clear case where I should use a more generic error
    -- type.
    maybeToParseResult :: Maybe a -> Either Error a
    maybeToParseResult = maybe (Left "") Right

  mkParser :: [RuleExpr Text] -> Parsec [Text] (RuleMap Text) [Text]
  mkParser = foldr
    (\x acc -> case x of
      Var x' -> do
        modifyState $ M.insert (Var x') [x']
        (:) <$> char' x' <*> acc
      MetaVar x' -> do
        -- `nonBracket'` and `nonTrueNonBracket'` are hacks because I couldn't
        -- figure out how to reluctantly match as little as possible. I think
        -- the parser-combinators package might contain what I need.
        x'' <- nonBracket'
        modifyState $ M.insert (MetaVar x') [x'']
        (x'' :) <$> acc
      MetaListVar x' -> do
        x'' <- many nonTrueNonBracket'
        modifyState $ M.insert (MetaListVar x') x''
        (x'' ++) <$> acc
      RuleSep -> error "assertion error: RuleSep found by mkParser"
      CondSep -> error "assertion error: CondSep found by mkParser"
    )
    (pure [])

-- | Split Joy code into tokens.
tokenize :: Text -> [Text]
tokenize = filter (not . T.null) . tokenize' where

  tokenize' :: Text -> [Text]
  tokenize' = unfoldr
    (\xxs -> case T.uncons xxs of
      Nothing        -> Nothing
      Just ('[', xs) -> Just ("[", xs)
      Just (']', xs) -> Just ("]", xs)
      Just _   -> Just (a, T.dropWhile (== ' ') b)
        where (a, b) = T.break (`elem` ("[] " :: String)) xxs
    )

-- I ended these with an apostrophe, because they work on tokens, not Strings.
satisfy' :: (Eq a, Show a) => (a -> Bool) -> Parsec [a] u a
satisfy' p = tokenPrim showTok posFromTok testTok
 where
  showTok t = show t
  posFromTok pos _ _ = incSourceColumn pos 1
  testTok t = mfilter p (Just t)

char' :: (Eq a, Show a) => a -> Parsec [a] u a
char' x = satisfy' (== x) <?> show x

nonBracket' :: Parsec [Text] u Text
nonBracket' = satisfy' (`notElem` ["[", "]"])

nonTrueNonBracket' :: Parsec [Text] u Text
nonTrueNonBracket' = satisfy' (`notElem` ["[", "]", "true"])