tock-mirror/TreeUtil.hs
Adam Sampson c8d5a4ff40 Add licensing information.
The compiler itself is under the GPLv2+; the support code that gets built into
user programs is under the LGPLv2+. This matches the existing practice for the
KRoC project.

(As with Occade, I've used the new GPLv3-style license header in the source
files, though, since that avoids having to update the FSF's postal address.)
2007-08-18 20:42:11 +00:00

273 lines
13 KiB
Haskell

{-
Tock: a compiler for parallel languages
Copyright (C) 2007 University of Kent
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 2 of the License, or (at your
option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <http://www.gnu.org/licenses/>.
-}
module TreeUtil (Pattern(..), MatchErrors, AnyDataItem(..), Items, assertPatternMatch, getMatchedItems, mkPattern, tag0, tag1, tag2, tag3, tag4, tag5, tag6, tag7) where
import Test.HUnit hiding (State)
import qualified Data.Map as Map
import Control.Monad.State
import Data.Generics
import qualified PrettyShow as PS
import Text.PrettyPrint.HughesPJ
import Data.Maybe
import Data.List
data Pattern =
-- | We don't care what the value is -- will match against any item
DontCare
-- | A named "item of interest". Given the specified key, all data items with the same key should be identical (otherwise match errors will be given)
| Named String Pattern
-- | A constructed item. This is special because the sub-items may not be of the right type for the constructor,
-- because they may be special items (such as DontCare)
| Match Constr [Pattern]
deriving (Typeable,Show,Eq)
--No proper gunfold, as I still can't figure out to implement it (Constr is problematic)
instance Data Pattern where
toConstr (DontCare) = constr_DontCare
toConstr (Named {}) = constr_Named
toConstr (Match {}) = constr_Match
gunfold _ _ _ = error "gunfold Pattern"
dataTypeOf _ = ty_Pattern
ty_Pattern = mkDataType "TreeUtil.Pattern"
[
constr_DontCare
,constr_Named
,constr_Match
]
constr_DontCare = mkConstr ty_Pattern "DontCare" [] Prefix
constr_Named = mkConstr ty_Pattern "Named" [] Prefix
constr_Match = mkConstr ty_Pattern "Match" [] Prefix
--Print the data nicely for TreeUtil.Pattern, to make it look like a pattern match:
pshowPattern :: Pattern -> String
pshowPattern = render . pshowPattern'
where
pshowPattern' :: Pattern -> Doc
pshowPattern' (DontCare) = text "_"
pshowPattern' (Named s p) = (text (s ++ "@")) <> (pshowPattern' p)
pshowPattern' (Match c ps) =
--All a bit hacky, admittedly:
if PS.isTupleCtr (showConstr c) then parens $ sep $ punctuate (text ",") items
--TODO add some decent list unfolding (to display Match (:) [x,Match (:) [y,Match [] []]] as [x,y]
else parens $ (text (showConstr c)) $+$ (sep items)
where items = map pshowPattern' ps
type MatchErrors = [String]
-- | A data item used when matching items of interest. Note that it is similar to Pattern but not the same;
-- AnyDataItem can only contain Data, and has no other special values.
data AnyDataItem = forall a. Data a => ADI a
instance Show AnyDataItem where
show (ADI a) = gshow a
instance Eq AnyDataItem where
(==) (ADI x) (ADI y) = case (cast y) of
-- Same type, use the library-provided geq function to compare them:
Just y' -> geq x y'
-- Different types, so not equal
Nothing -> False
-- | A type representing the state involved in recording items of interest. It is simply a map
-- from arbitrary string keys to AnyDataItem, which is just a wrapper for any item of class Data
type Items = Map.Map String AnyDataItem
-- | A function that takes a string key (of an item of interest), and a value.
-- If the key has NOT been seen before, checkItem merely records its value in the set of items of interest
-- If the key has been seen before, checkItem checks that the new (passed) value matches the old value.
checkItem :: String -> AnyDataItem -> State Items MatchErrors
checkItem key val
= do items <- get
case Map.lookup key items of
Just foundVal -> return $
if foundVal == val then []
--show key will put quote marks around it, and escape any special characters (which seems useful here):
else ["Item of interest does not match prior value for key " ++ (show key) ++ ", prior: " ++ (show foundVal) ++ " current: " ++ (show val)]
Nothing -> do {put (Map.insert key val items) ; return [] }
-- | A function that takes an expected Pattern value, an actual Data value, and returns the appropriate checks
-- for pseudo-equality. This pseudo-equality is equality, enhanced by the possibility of Pattern's
-- DontCare and Named (item-of-interest) values which match differently.
checkMatch :: Data z => Pattern -> z -> State Items MatchErrors
-- | DontCare matches anything, so return an empty assertion:
checkMatch DontCare _ = return []
-- | Items of interest are delegated to the checkItem function that uses the Items state
checkMatch (Named s p) b = sequenceS [checkMatch p b, checkItem s (ADI b)]
-- | Constructors are matched using the applyAll function (but we must also check the constructors are equal)
checkMatch m@(Match con items) b
-- Check the patterns are consistent; see note #1 below this checkMatch function
= case ((not $ isAlgType (dataTypeOf b)) || (elem con (dataTypeConstrs $ dataTypeOf b))) of
False -> return ["Inconsistent pattern (your program has been written wrongly), constructor not possible here: "
++ show con ++ " possible constructors are: " ++ show (dataTypeConstrs $ dataTypeOf b)
++ " in pattern:\n" ++ pshowPattern m ++ "\n*** trying to match against actual value:\n" ++ PS.pshow b]
True ->
case (checkConsEq con (toConstr b) m b) of
Nothing -> sequenceS $ (applyAll items b)
Just err -> return [err] --no point comparing fields if the constructors don't match
where
--The whole things are given as third/fourth parameters just so we can produce a more helpful error message:
checkConsEq :: Data z => Constr -> Constr -> Pattern -> z -> Maybe String
checkConsEq a b a' b' = if (a == b)
then Nothing
else Just $ "Constructors not equal, expected constructor: " ++ (show a) ++ " actual cons: " ++ (show b)
++ " while trying to match expected:\n" ++ (pshowPattern a') ++ "\n*** against actual:\n " ++ (PS.pshow b')
-- | applyAll checks that the non-constructor items of an algebraic data type are matched:
applyAll :: Data z => [Pattern] -> z -> [State Items MatchErrors]
applyAll list d = map (applyAll' d) (zip [0..] list)
applyAll' :: Data z => z -> (Int,Pattern) -> State Items MatchErrors
applyAll' d (index,f)
= if (index >= numIndexes)
then return ["Invalid index in applyAll: " ++ (show index) ++ ", numIndexes is: " ++ (show numIndexes)
++ " trying to check, expected: " ++ (pshowPattern f) ++ " actual: " ++ (PS.pshow d)]
else (gmapQi index (checkMatch f) d)
--Possibly a better way?
where
numIndexes = length (gmapQ (const 0) d)
{-
Note #1 (referenced above):
There was a problem with using the tag functions for building patterns:
Given a data structure:
data Vegetable = Carrot | Potato | Broccoli
data Meat = Beef | Pork
data Meal = MeatTwoVeg Meat (Vegetable,Vegetable)
there was nothing to stop you doing any of this:
tag2 MeatTwoVeg Carrot (Potato, Broccoli)
tag2 MeatTwoVeg Carrot (Beef, Potato)
tag2 MeatTwoVeg Beef [Carrot, Pork]
And so on and so forth. Of course, an inconsistent pattern for Meal such as those above can never match Meal; it is an error in the program.
I could have prevented this by wrapping the tag functions in an error monad, or by doing checks inside the tag functions that could die using error,
but a better solution seemed to be to gather the error checking into assertPatternMatch/getMatchedItems, since they already have the option
to return errors. So you can build an inconsistent pattern, but when you come to use it you will get an error highlighting your mistake of
creating an inconsistent pattern (rather than simply a did-not-match error)
-}
-- | A helper function for concating awkward lists in monads
sequenceS :: [State Items MatchErrors] -> State Items MatchErrors
--We have [State Items MatchErrors]
--We want State Items MatchErrors
--sequence :: [State Items MatchErrors] -> State Items [MatchErrors]
--concat :: [MatchErrors] -> MatchErrors
sequenceS x = (liftM concat) (sequence x)
-- | A function for checking that two Data items (expected, actual) match, where the expected item (LHS)
-- may contain special Pattern values (such as DontCare, Named, etc)
assertPatternMatch :: (Data y, Data z) => String -> y -> z -> Assertion
assertPatternMatch msg exp act =
-- Uncomment this line for debugging help:
-- putStrLn ("Testing: " ++ (PS.pshow a) ++ " vs " ++ (PS.pshow b)) >>
sequence_ $ map (assertFailure . ((++) (msg ++ " "))) (evalState (checkMatch (mkPattern exp) act) (Map.empty))
-- | A function for getting the matched items from the patterns on the LHS
-- Either returns the matched items, or a list of errors from the matching
getMatchedItems :: (Data y, Data z) => y -> z -> Either MatchErrors Items
getMatchedItems a b
= case errors of
[] -> Right items
_ -> Left errors
where (errors,items) = runState (checkMatch (mkPattern a) b) (Map.empty)
--The mkPattern function needs to be able to take things such as:
-- v :: Pattern
-- (w:ws) :: [Pattern]
-- (x0,x1) :: (Pattern,Pattern)
-- (y0,y1,y2) :: Data b => (Pattern,Pattern,b)
--And return an Pattern:
-- v
-- Match (:) [w,mkPattern ws]
-- Match (,) [x0,x1]
-- Match (,,) [y0,y1,mkPattern u]
--The latter three could be rephrased as:
-- Match (:) [mkPattern w,mkPattern ws]
-- Match (,) [mkPattern x0,mkPattern x1]
-- Match (,,) [mkPattern y0,mkPattern y1,mkPattern u]
--Therefore the whole function can be viewed as: get the constructor, and map mkPattern over the sub-elements
mkPattern :: (Data a) => a -> Pattern
mkPattern x = case ((cast x) :: Maybe Pattern) of
Just x' -> x'
Nothing -> Match (toConstr x) (gmapQ mkPattern x)
--I'm not sure tag0 is ever needed, but just in case:
tag0 :: (Data a) => a -> Pattern
tag0 con = (Match (toConstr con) [])
-- | A helper function. The parameters are: constructor item0 item1, where the constructor has arity 2
tag1 :: (Data a, Data b0) => (a0 -> a) -> b0 -> Pattern
tag1 con x0 = (Match (toConstr con') [mkPattern x0])
where
con' = con (undefined :: a0)
-- | A helper function. The parameters are: constructor item0 item1, where the constructor has arity 2
tag2 :: (Data a, Data b0, Data b1) => (a0 -> a1 -> a) -> b0 -> b1 -> Pattern
tag2 con x0 x1 = (Match (toConstr con') [mkPattern x0,mkPattern x1])
where
con' = con (undefined :: a0) (undefined :: a1)
-- | A helper function. The parameters are: constructor item0 item1 item2, where the constructor has arity 3
tag3 :: (Data a, Data b0, Data b1, Data b2) => (a0 -> a1 -> a2 -> a) -> b0 -> b1 -> b2 -> Pattern
tag3 con x0 x1 x2 = (Match (toConstr con') [mkPattern x0,mkPattern x1,mkPattern x2])
where
con' = con (undefined :: a0) (undefined :: a1) (undefined :: a2)
-- | A helper function. The parameters are: constructor item0 item1 item2 item3, where the constructor has arity 4
tag4 :: (Data a, Data b0, Data b1, Data b2, Data b3) => (a0 -> a1 -> a2 -> a3 -> a) -> b0 -> b1 -> b2 -> b3 -> Pattern
tag4 con x0 x1 x2 x3 = (Match (toConstr con') [mkPattern x0,mkPattern x1,mkPattern x2,mkPattern x3])
where
con' = con (undefined :: a0) (undefined :: a1) (undefined :: a2) (undefined :: a3)
-- | A helper function. The parameters are: constructor item0 item1 item2 item3 item4, where the constructor has arity 5
tag5 :: (Data a, Data b0, Data b1, Data b2, Data b3, Data b4) =>
(a0 -> a1 -> a2 -> a3 -> a4 -> a) -> b0 -> b1 -> b2 -> b3 -> b4 -> Pattern
tag5 con x0 x1 x2 x3 x4 = (Match (toConstr con') [mkPattern x0,mkPattern x1,mkPattern x2,mkPattern x3,mkPattern x4])
where
con' = con (undefined :: a0) (undefined :: a1) (undefined :: a2) (undefined :: a3) (undefined :: a4)
-- | A helper function. The parameters are: constructor item0 item1 item2 item3 item4 item5, where the constructor has arity 6
tag6 :: (Data a, Data b0, Data b1, Data b2, Data b3, Data b4, Data b5) =>
(a0 -> a1 -> a2 -> a3 -> a4 -> a5 -> a) -> b0 -> b1 -> b2 -> b3 -> b4 -> b5 -> Pattern
tag6 con x0 x1 x2 x3 x4 x5 = (Match (toConstr con') [mkPattern x0,mkPattern x1,mkPattern x2,mkPattern x3,mkPattern x4,mkPattern x5])
where
con' = con (undefined :: a0) (undefined :: a1) (undefined :: a2) (undefined :: a3) (undefined :: a4) (undefined :: a5)
-- | A helper function. The parameters are: constructor item0 item1 item2 item3 item4 item5 item6, where the constructor has arity 7
tag7 :: (Data a, Data b0, Data b1, Data b2, Data b3, Data b4, Data b5, Data b6) =>
(a0 -> a1 -> a2 -> a3 -> a4 -> a5 -> a6 -> a) -> b0 -> b1 -> b2 -> b3 -> b4 -> b5 -> b6 -> Pattern
tag7 con x0 x1 x2 x3 x4 x5 x6 = (Match (toConstr con') [mkPattern x0,mkPattern x1,mkPattern x2,mkPattern x3,mkPattern x4,mkPattern x5,mkPattern x6])
where
con' = con (undefined :: a0) (undefined :: a1) (undefined :: a2) (undefined :: a3) (undefined :: a4) (undefined :: a5) (undefined :: a6)