hydra-0.12.0: src/test/haskell/Hydra/RewritingSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-
stack ghci hydra:lib hydra:hydra-test
Test.Hspec.hspec Hydra.RewritingSpec.spec
-}
module Hydra.RewritingSpec where
import Hydra.Kernel
import Hydra.Monads
import Hydra.Tools.Monads
import Hydra.Dsl.Terms as Terms
import qualified Hydra.Dsl.Types as Types
import Hydra.Dsl.ShorthandTypes
import qualified Hydra.Show.Core as ShowCore
import Hydra.TestUtils
import qualified Test.Hspec as H
import qualified Test.QuickCheck as QC
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Set as S
import qualified Data.Maybe as Y
data Quux a = QuuxUnit | QuuxValue a | QuuxPair (Quux a) (Quux a) deriving (Eq, Ord, Show)
fsubQuux :: (a -> b) -> (Quux a -> Quux b) -> Quux a -> Quux b
fsubQuux mf recurse q = case q of
QuuxUnit -> QuuxUnit
QuuxValue x -> QuuxValue $ mf x
QuuxPair left right -> QuuxPair (recurse left) (recurse right)
rewriteQuux :: (a -> b) -> ((Quux a -> Quux b) -> Quux a -> Quux b) -> Quux a -> Quux b
rewriteQuux mf f = rewrite (fsubQuux mf) f
myQuuxRewriter :: Quux String -> Quux Int
myQuuxRewriter = rewriteQuux L.length $ \fsub q -> fsub $ case q of
QuuxPair left right -> QuuxPair QuuxUnit right
_ -> q
checkFoldOverTerm :: H.SpecWith ()
checkFoldOverTerm = do
H.describe "Test foldOverTerm" $ do
H.describe "Pre-order" $ do
H.it "test #1" $
H.shouldBe
(traverse TraversalOrderPre node1)
["a"]
H.it "test #2" $
H.shouldBe
(traverse TraversalOrderPre node2)
["a", "b", "c", "d"]
H.describe "Post-order" $ do
H.it "test #1" $
H.shouldBe
(traverse TraversalOrderPost node1)
["a"]
H.it "test #1" $
H.shouldBe
(traverse TraversalOrderPost node2)
["b", "d", "c", "a"]
where
node label children = Terms.pair (Terms.string label) (Terms.list children)
labelOf term = case term of
TermProduct [TermLiteral (LiteralString label), _] -> Just label
_ -> Nothing
traverse :: TraversalOrder -> Term -> [String]
traverse order = Y.catMaybes . foldOverTerm order (\l t -> l ++ [labelOf t]) []
node1 = node "a" []
node2 = node "a" [node "b" [], node "c" [node "d" []]]
checkStripTerm :: H.SpecWith ()
checkStripTerm = do
H.describe "Tests for stripping annotations from terms" $ do
H.it "Un-annotated terms are not affected" $
QC.property $ \term -> case (term :: Term) of
TermAnnotated _ -> True
_ -> deannotateTerm term == term
H.it "Terms are stripped recursively" $
QC.property $ \term -> case (term :: Term) of
TermAnnotated _ -> True
_ -> deannotateTerm (Terms.annot M.empty (Terms.annot M.empty term)) == term
checkStripType :: H.SpecWith ()
checkStripType = do
H.describe "Tests for stripping annotations from types" $ do
H.it "Un-annotated types are not affected" $
QC.property $ \typ -> case (typ :: Type) of
TypeAnnotated _ -> True
_ -> deannotateType typ == typ
H.it "Types are stripped recursively" $
QC.property $ \typ -> case (typ :: Type) of
TypeAnnotated _ -> True
_ -> deannotateType (Types.annot M.empty (Types.annot M.empty typ)) == typ
testExpandLambdas :: Graph -> H.SpecWith ()
testExpandLambdas g = do
H.describe "Test expanding to (untyped) lambda terms" $ do
H.describe "Try terms which do not expand" $ do
H.it "test #1" $
noChange (int32 42)
H.it "test #2" $
noChange (list ["foo", "bar"])
H.it "test #3" $
noChange (splitOn @@ "foo" @@ "bar")
H.it "test #4" $
noChange (lambda "x" $ lambda "y" $ splitOn @@ var "x" @@ var "y")
H.it "test #5" $
noChange (lambda "x" $ int32 42)
H.describe "Try bare function terms" $ do
H.it "test #1" $
expandsTo
toLower
(lambda "v1" $ toLower @@ var "v1")
H.it "test #2" $
expandsTo
splitOn
(lambda "v1" $ lambda "v2" $ splitOn @@ var "v1" @@ var "v2")
H.it "test #3" $
expandsTo
(splitOn @@ var "foo")
(lambda "v1" $ splitOn @@ var "foo" @@ var "v1")
H.it "test #4" $
expandsTo
(splitOn @@ var "foo" @@ var "bar")
(splitOn @@ var "foo" @@ var "bar")
H.it "test #5" $
expandsTo
(splitOn @@ var "foo" @@ var "bar" @@ var "baz")
(splitOn @@ var "foo" @@ var "bar" @@ var "baz")
H.it "test #6" $
expandsTo
(primitive _optionals_maybe @@ (int32 42) @@ length)
-- Note two levels of lambda expansion
(lambda "v1" $ (primitive _optionals_maybe @@ (int32 42) @@ (lambda "v1" $ length @@ var "v1")) @@ var "v1")
H.it "test #7" $
expandsTo
(project (Name "Person") (Name "firstName"))
(lambda "v1" $ ((project (Name "Person") (Name "firstName") @@ var "v1")))
-- TODO: case statement
H.describe "Try subterms within applications" $ do
H.it "test #1" $
expandsTo
(splitOn @@ "bar")
(lambda "v1" $ splitOn @@ "bar" @@ var "v1")
H.it "test #2" $
expandsTo
(lambda "x" $ splitOn @@ var "x")
(lambda "x" $ lambda "v1" $ splitOn @@ var "x" @@ var "v1")
H.it "test #3" $
expandsTo
((lambda "x" $ var "x") @@ length)
(lambda "v1" $ length @@ var "v1")
H.describe "Try let terms" $ do
H.it "test #1" $
noChange
(lets ["foo">: int32 137] $ int32 42)
H.it "test #2" $
expandsTo
(lets ["foo">: splitOn] $ var "foo")
(lets ["foo">: lambda "v1" $ lambda "v2" $ splitOn @@ var "v1" @@ var "v2"] $ var "foo")
H.describe "Check that complete applications are no-ops" $ do
H.it "test #1" $
noChange
(toLower @@ "FOO")
H.it "test #2" $
noChange
(splitOn @@ "foo" @@ "bar")
H.describe "Try other subterms" $ do
H.it "test #1" $
expandsTo
(list [lambda "x" $ list ["foo"], splitOn @@ "bar"])
(list [lambda "x" $ list ["foo"], lambda "v1" $ splitOn @@ "bar" @@ var "v1"])
H.it "Check that lambda expansion is idempotent" $ do
QC.property $ \term -> do
let once = expandLambdas g term
let twice = expandLambdas g once
H.shouldBe once twice
where
length = primitive $ Name "hydra.lib.strings.length"
splitOn = primitive $ Name "hydra.lib.strings.splitOn"
toLower = primitive $ Name "hydra.lib.strings.toLower"
fromList = primitive $ Name "hydra.lib.sets.fromList"
expandsTo termBefore termAfter = do
let result = expandLambdas g termBefore
H.shouldBe (ShowCore.term result) (ShowCore.term termAfter)
noChange term = expandsTo term term
-- TODO: merge this into expandLambdas
testExpandTypedLambdas :: H.SpecWith ()
testExpandTypedLambdas = do
H.describe "Test expanding to typed lambda terms" $ do
H.describe "Try some terms which do not expand" $ do
H.it "test #1" $
noChange (int32 42)
H.it "test #2" $
noChange (list ["foo", "bar"])
H.it "test #3" $
noChange (splitOn @@ "foo" @@ "bar")
H.it "test #4" $
noChange (lambda "x" $ int32 42)
H.describe "Expand bare function terms" $ do
H.it "test #1" $
expandsTo
toLower
(lambda "v1" $ toLower @@ var "v1")
H.it "test #2" $
expandsTo
splitOn
(lambda "v1" $ lambda "v2" $ splitOn @@ var "v1" @@ var "v2")
H.it "test #3" $
expandsTo
(primitive _optionals_maybe @@ (int32 42) @@ length)
-- Note two levels of lambda expansion
(lambda "v1" $ (primitive _optionals_maybe @@ (int32 42) @@ (lambda "v1" $ length @@ var "v1")) @@ var "v1")
H.it "test #4" $
expandsTo
(project (Name "Person") (Name "firstName"))
(lambda "v1" $ ((project (Name "Person") (Name "firstName") @@ var "v2")))
-- TODO: case statement
H.describe "Expand subterms within applications" $ do
H.it "test #1" $
expandsTo
(splitOn @@ "bar")
(lambda "v1" $ splitOn @@ "bar" @@ var "v1")
H.it "test #2" $
expandsTo
((lambda "x" $ var "x") @@ length)
((lambda "x" $ var "x") @@ (lambda "v1" $ length @@ var "v1"))
H.describe "Expand arbitrary subterms" $ do
H.it "test #1" $
expandsTo
(list [lambda "x" $ list ["foo"], splitOn @@ "bar"])
(list [lambda "x" $ list ["foo"], lambda "v1" $ splitOn @@ "bar" @@ var "v1"])
H.it "Check that lambda expansion is idempotent" $ do
QC.property $ \term -> do
let once = expandTypedLambdas term
let twice = expandTypedLambdas once
H.shouldBe once twice
where
length = primitive $ Name "hydra.lib.strings.length"
splitOn = primitive $ Name "hydra.lib.strings.splitOn"
toLower = primitive $ Name "hydra.lib.strings.toLower"
expandsTo termBefore termAfter = do
-- result <- flowToIo testGraph $ expandLambdas termBefore
-- H.shouldBe result termAfter
inf <- flowToIo testGraph (inferenceResultTerm <$> inferInGraphContext termBefore)
let result = expandTypedLambdas inf
H.shouldBe (ShowCore.term (removeTermAnnotations result)) (ShowCore.term termAfter)
noChange term = expandsTo term term
testFoldOverTerm :: H.SpecWith ()
testFoldOverTerm = do
H.describe "Test folding over terms" $ do
H.it "Try a simple fold" $ do
H.shouldBe
(foldOverTerm TraversalOrderPre adds 0
(list [int32 42, (lambda "x" $ var "x") @@ int32 10]))
52
H.it "Check that traversal order is respected" $ do
H.shouldBe
(foldOverTerm TraversalOrderPre listLengths []
(list [list [string "foo", string "bar"], (lambda "x" $ var "x") @@ (list [string "quux"])]))
[1, 2, 2]
H.shouldBe
(foldOverTerm TraversalOrderPost listLengths []
(list [list [string "foo", string "bar"], (lambda "x" $ var "x") @@ (list [string "quux"])]))
[2, 1, 2]
where
adds sum term = case term of
TermLiteral (LiteralInteger (IntegerValueInt32 i)) -> sum + i
_ -> sum
listLengths l term = case term of
TermList els -> L.length els:l
_ -> l
testFlattenLetTerms :: H.SpecWith ()
testFlattenLetTerms = do
H.describe "Test flattening of 'let' terms" $ do
H.it "Non-let terms are unaffected" $ do
H.shouldBe
(flattenLetTerms $ Terms.int32 42)
(Terms.int32 42)
H.shouldBe
(flattenLetTerms $ Terms.list [Terms.string "foo"])
(Terms.list [Terms.string "foo"])
H.it "Non-nested let terms are unaffected" $
H.shouldBe
(flattenLetTerms letTerm1)
(letTerm1)
H.it "Nonrecursive, nested bindings are flattened" $
H.shouldBe
(flattenLetTerms letTerm2)
(letTerm2_flattened)
H.it "Multiple levels of nesting are flattened appropriately" $
H.shouldBe
(flattenLetTerms letTerm3)
(letTerm3_flattened)
where
makeLet body pairs = TermLet $ Let (makeBinding <$> pairs) body
where
makeBinding (k, v) = Binding (Name k) v Nothing
letTerm1 = makeLet (TermList [Terms.var "x", Terms.var "y"]) [
("x", Terms.int32 1),
("y", Terms.int32 2)]
letTerm2 = makeLet (TermList [Terms.var "a", Terms.var "b"]) [
("a", Terms.int32 1),
("b", letTerm1)]
letTerm2_flattened = makeLet (TermList [Terms.var "a", Terms.var "b"]) [
("a", Terms.int32 1),
("b", TermList [Terms.var "b_x", Terms.var "b_y"]),
("b_x", Terms.int32 1),
("b_y", Terms.int32 2)]
letTerm3 = makeLet (TermList [Terms.var "a", Terms.var "b"]) [
("a", Terms.int32 1),
("b", makeLet (TermList [Terms.var "x", Terms.var "y"]) [
("x", Terms.int32 1),
("y", makeLet (TermList [Terms.var "a", Terms.var "q"]) [
("p", Terms.int32 137),
("q", TermList [Terms.var "x", Terms.int32 5])])])]
letTerm3_flattened = makeLet (TermList [Terms.var "a", Terms.var "b"]) [
("a", Terms.int32 1),
("b", TermList [Terms.var "b_x", Terms.var "b_y"]),
("b_x", Terms.int32 1),
("b_y", TermList [Terms.var "a", Terms.var "b_y_q"]),
("b_y_p", Terms.int32 137),
("b_y_q", TermList [Terms.var "b_x", Terms.int32 5])]
testFreeVariablesInTerm :: H.SpecWith ()
testFreeVariablesInTerm = do
H.describe "Test free variables" $ do
-- H.it "Generated terms never have free variables" $ do
-- QC.property $ \(TypedTerm term _) -> do
-- H.shouldBe
-- (freeVariablesInTerm (term))
-- S.empty
H.it "Free variables in individual terms" $ do
H.shouldBe
(freeVariablesInTerm (string "foo"))
S.empty
H.shouldBe
(freeVariablesInTerm (var "x"))
(S.fromList [Name "x"])
H.shouldBe
(freeVariablesInTerm (list [var "x", (lambda "y" $ var "y") @@ int32 42]))
(S.fromList [Name "x"])
H.shouldBe
(freeVariablesInTerm (list [var "x", (lambda "y" $ var "y") @@ var "y"]))
(S.fromList [Name "x", Name "y"])
testNormalizeTypeVariablesInTerm :: H.SpecWith ()
testNormalizeTypeVariablesInTerm = do
H.describe "No type variables" $ do
H.it "test #1" $ noChange
(int32 42)
H.it "test #2" $ noChange
(tlet (int32 42) [
("foo", Nothing, string "foo")])
H.it "test #3" $ noChange
(tlet (int32 42) [
("foo", Just tsString, string "foo")])
H.it "test #4" $ noChange
(withMonoFoo $ int32 42)
H.describe "Only free type variables" $ do
H.it "test #1" $ noChange
(withPolyFoo $ int32 42)
H.it "test #2" $ noChange
(withMonoFoo const42)
H.it "test #3" $ noChange
(withPolyFoo const42)
H.describe "Simple polymorphic let bindings" $ do
H.it "test #1" $ changesTo
(withIdBefore id42)
(withIdAfter id42)
H.describe "Rewriting of bindings does not affect environment" $ do
H.it "test #1" $ changesTo
(withIdBefore const42) -- Free variable "a" coincides with bound variable "a", but in a different branch.
(withIdAfter const42)
H.it "test #2" $ changesTo -- Same substitution in bindings and environment
(withIdBefore (withIdBefore id42))
(withIdAfter (withIdAfter id42))
H.describe "Nested polymorphic let bindings" $ do
H.it "Parent variable shadows child variable" $ changesTo
(tlet id42 [
("id", Just faa, tlet (lambdaTyped "y" tA $ var "id2" @@ var "y") [
("id2", Just faa, lambdaTyped "x" tA $ var "x")])])
(tlet id42 [
("id", Just ft0t0, tlet (lambdaTyped "y" t0 $ var "id2" @@ var "y") [
("id2", Just ft1t1, lambdaTyped "x" t1 $ var "x")])])
H.it "No shadowing" $ changesTo
(tlet id42 [
("id", Just faa, tlet (lambdaTyped "y" tA $ var "id2" @@ var "y") [
("id2", Just fbb, lambdaTyped "x" tB $ var "x")])])
(tlet id42 [
("id", Just ft0t0, tlet (lambdaTyped "y" t0 $ var "id2" @@ var "y") [
("id2", Just ft1t1, lambdaTyped "x" t1 $ var "x")])])
H.it "No shadowing, locally free type variable" $ changesTo
(tlet (var "fun1" @@ string "foo" @@ int32 42) [
("fun1", Just (Types.poly ["a", "b"] $ Types.functionMany [tA, tB, tPair tA tB]), lambdaTyped "x" tA $ lambdaTyped "y" tB $
tlet (var "fun2" @@ var "x") [
("fun2", Just (Types.poly ["c"] $ tFun tC $ tPair tC tB), lambdaTyped "z" tC $ pair (var "z") (var "y"))])])
(tlet (var "fun1" @@ string "foo" @@ int32 42) [
("fun1", Just (Types.poly ["t0", "t1"] $ Types.functionMany [t0, t1, tPair t0 t1]), lambdaTyped "x" t0 $ lambdaTyped "y" t1 $
tlet (var "fun2" @@ var "x") [
("fun2", Just (Types.poly ["t2"] $ tFun t2 $ tPair t2 t1), lambdaTyped "z" t2 $ pair (var "z") (var "y"))])])
where
changesTo term1 term2 = H.shouldBe (normalize term1) term2
noChange term = H.shouldBe (normalize term) term
normalize = normalizeTypeVariablesInTerm
tlet env triples = TermLet $ Let (toBinding <$> triples) env
where
toBinding (key, mts, value) = Binding (Name key) value mts
t0 = Types.var "t0"
t1 = Types.var "t1"
t2 = Types.var "t2"
const42 = lambdaTyped "x" (Types.function tA tInt32) $ int32 42
faa = Types.poly ["a"] $ tFun tA tA
fbb = Types.poly ["b"] $ tFun tB tB
ft0t0 = Types.poly ["t0"] $ tFun t0 t0
ft1t1 = Types.poly ["t1"] $ tFun t1 t1
id42 = var "id" @@ int32 42
tsString = Types.mono Types.string
tsA = Types.mono $ Types.var "a"
withIdBefore term = tlet term [
("id", Just faa, lambda "x" $ var "x")]
withIdAfter term = tlet term [
("id", Just ft0t0, lambda "x" $ var "x")]
withMonoFoo term = tlet term [
("foo", Just tsString, string "foo")]
withPolyFoo term = tlet term [
("foo", Just tsA, var "bar")]
testReplaceTerm :: H.SpecWith ()
testReplaceTerm = do
H.describe "Test term replacement" $ do
H.it "Check that the correct subterms are replaced" $ do
H.shouldBe
(rewriteTerm replaceInts
(int32 42))
(int64 42)
H.shouldBe
(rewriteTerm replaceInts
(list [int32 42, (lambda "x" $ var "x") @@ int32 137]))
(list [int64 42, (lambda "x" $ var "x") @@ int64 137])
H.it "Check that traversal order is respected" $ do
H.shouldBe
(rewriteTerm replaceListsPre
(list [list [list []]]))
(list [list []])
H.shouldBe
(rewriteTerm replaceListsPost
(list [list [list []]]))
(list [])
-- H.it "Check that metadata is replace recursively" $ do
-- H.shouldBe
-- (rewriteTerm keepTerm replaceKv (list [annot 42 (string "foo")] Int))
-- (list [annot "42" (string "foo")])
where
keepTerm recurse term = recurse term
replaceInts recurse term = case term2 of
TermLiteral (LiteralInteger (IntegerValueInt32 v)) -> int64 $ fromIntegral v
_ -> term2
where
term2 = recurse term
replaceLists term = case term of
TermList (h:_) -> case h of
TermList [] -> list []
_ -> term
_ -> term
replaceListsPre recurse = recurse . replaceLists
replaceListsPost recurse = replaceLists . recurse
replaceKv i = show i
testRewriteExampleType :: H.SpecWith ()
testRewriteExampleType = do
H.describe "Test rewriting of a made-up recursive type" $ do
H.it "Rewrite a hand-picked expression" $ do
H.shouldBe
quux2
(myQuuxRewriter quux1)
where
quux1 = QuuxPair QuuxUnit (QuuxPair (QuuxValue "abc") (QuuxValue "12345"))
quux2 = QuuxPair QuuxUnit (QuuxPair QuuxUnit (QuuxValue 5))
testSimplifyTerm :: H.SpecWith ()
testSimplifyTerm = do
H.describe "Test term simplifation (optimization)" $ do
H.it "Check that 'const' applications are simplified" $ do
H.shouldBe
(simplifyTerm $ (lambda "x" $ string "foo") @@ int32 42)
(string "foo")
H.shouldBe
(simplifyTerm ((lambda "x" $ list [var "x", var "x"]) @@ var "y"))
(list [var "y", var "y"])
H.shouldBe
(simplifyTerm ((lambda "x" $ string "foo") @@ var "y"))
(string "foo")
H.shouldBe
(simplifyTerm ((lambda "x"
((lambda "a" (list [string "foo", var "a"])) @@ var "x")) @@ var "y"))
(list [string "foo", var "y"])
--testStripAnnotations :: H.SpecWith ()
--testStripAnnotations = do
-- H.describe "Test stripping metadata from terms" $ do
--
-- H.it "Strip type annotations" $ do
-- QC.property $ \(TypedTerm term typ) -> do
-- shouldSucceedWith
-- (getTermType term)
-- Nothing
-- shouldSucceedWith
-- (getTermType $ withType typ term)
-- (Just typ)
-- shouldSucceedWith
-- (getTermType $ strip $ withType typ term)
-- Nothing
testTopologicalSortBindings :: H.SpecWith ()
testTopologicalSortBindings = do
H.describe "Test topological sort of bindings" $ do
H.it "Isolated bindings" $ do
checkBindings
[("a", string "foo"), ("b", string "bar")]
[["a"], ["b"]]
H.it "Single recursive binding" $ do
checkBindings
[("a", list [var "a"])]
[["a"]]
H.it "Mutually recursive bindings" $ do
checkBindings
[("a", list [var "b"]), ("b", list [var "a"])]
[["a", "b"]]
H.it "Mixed bindings" $ do
checkBindings
[("a", var "b"), ("b", list [var "a", var "c"]), ("c", string "foo"), ("d", string "bar")]
[["c"], ["a", "b"], ["d"]]
where
checkBindings bindings expectedVars = H.shouldBe
(topologicalSortBindingMap bindingMap)
expected
where
bindingMap = M.mapKeys (\k -> Name k) $ M.fromList bindings
expected = fmap (fmap (\k -> (Name k, Y.fromMaybe unit $ M.lookup (Name k) bindingMap))) expectedVars
spec :: H.Spec
spec = do
checkFoldOverTerm
checkStripTerm
checkStripType
testFoldOverTerm
testExpandLambdas testGraph
-- testExpandTypedLambdas -- TODO: restore me / merge with testExpandLambdas
testFlattenLetTerms
testFreeVariablesInTerm
testNormalizeTypeVariablesInTerm
testReplaceTerm
testRewriteExampleType
testSimplifyTerm
-- testStripAnnotations -- TODO: restore me
testTopologicalSortBindings