1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
|
-----------------------------------------------------------------------------
-- Module : HughesPJQuickCheck
-- Copyright : (c) 2008 Benedikt Huber
-- License : BSD-style
--
-- QuickChecks for HughesPJ pretty printer.
--
-- 1) Testing laws (blackbox)
-- - CDoc (combinator datatype)
-- 2) Testing invariants (whitebox)
-- 3) Testing bug fixes (whitebox)
--
-----------------------------------------------------------------------------
import PrettyTestVersion
import TestGenerators
import TestStructures
import UnitLargeDoc
import UnitPP1
import UnitT3911
import UnitT32
import Control.Monad
import Data.Char (isSpace)
import Data.List (intersperse)
import Debug.Trace
import Test.QuickCheck
main :: IO ()
main = do
-- quickcheck tests
check_laws
check_invariants
check_improvements
check_non_prims -- hpc full coverage
check_rendering
check_list_def
-- unit tests
testPP1
testT3911
testT32
testLargeDoc
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Utility functions
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- tweaked to perform many small tests
myConfig :: Int -> Int -> Args
myConfig d n = stdArgs { maxSize = d, maxDiscardRatio = n*5 }
maxTests :: Int
maxTests = 1000
myTest :: (Testable a) => String -> a -> IO ()
myTest = myTest' 15 maxTests
myTest' :: (Testable a) => Int -> Int -> String -> a -> IO ()
myTest' d n msg t = do
putStrLn (" * " ++ msg)
r <- quickCheckWithResult (myConfig d n) t
case r of
(Failure {}) -> error "Failed testing!"
_ -> return ()
myAssert :: String -> Bool -> IO ()
myAssert msg b = putStrLn $ (if b then "Ok, passed " else "Failed test:\n ") ++ msg
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Quickcheck tests
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Equalities on Documents
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- compare text details
tdEq :: TextDetails -> TextDetails -> Bool
tdEq td1 td2 = (tdToStr td1) == (tdToStr td2)
-- algebraic equality on reduced docs
docEq :: RDoc () -> RDoc () -> Bool
docEq rd1 rd2 = case (rd1, rd2) of
(Empty, Empty) -> True
(NoDoc, NoDoc) -> True
(NilAbove ds1, NilAbove ds2) -> docEq ds1 ds2
(TextBeside td1 ds1, TextBeside td2 ds2) | annotToTd td1 `tdEq` annotToTd td2 -> docEq ds1 ds2
(Nest k1 d1, Nest k2 d2) | k1 == k2 -> docEq d1 d2
(Union d11 d12, Union d21 d22) -> docEq d11 d21 && docEq d12 d22
(d1,d2) -> False
-- algebraic equality, with text reduction
deq :: Doc () -> Doc () -> Bool
deq d1 d2 = docEq (reduceDoc' d1) (reduceDoc' d2) where
reduceDoc' = mergeTexts . reduceDoc
deqs :: [Doc ()] -> [Doc ()] -> Bool
deqs ds1 ds2 =
case zipE ds1 ds2 of
Nothing -> False
(Just zds) -> all (uncurry deq) zds
zipLayouts :: Doc () -> Doc () -> Maybe [(Doc (),Doc ())]
zipLayouts d1 d2 = zipE (reducedDocs d1) (reducedDocs d2)
where reducedDocs = map mergeTexts . flattenDoc
zipE :: [Doc ()] -> [Doc ()] -> Maybe [(Doc (), Doc ())]
zipE l1 l2 | length l1 == length l2 = Just $ zip l1 l2
| otherwise = Nothing
-- algebraic equality for layouts (without permutations)
lseq :: Doc () -> Doc () -> Bool
lseq d1 d2 = maybe False id . fmap (all (uncurry docEq)) $ zipLayouts d1 d2
-- abstract render equality for layouts
-- should only be performed if the number of layouts is reasonably small
rdeq :: Doc () -> Doc () -> Bool
rdeq d1 d2 = maybe False id . fmap (all (uncurry layoutEq)) $ zipLayouts d1 d2
where layoutEq d1 d2 = (abstractLayout d1) == (abstractLayout d2)
layoutsCountBounded :: Int -> [Doc ()] -> Bool
layoutsCountBounded k docs = isBoundedBy k (concatMap flattenDoc docs)
where
isBoundedBy k [] = True
isBoundedBy 0 (x:xs) = False
isBoundedBy k (x:xs) = isBoundedBy (k-1) xs
layoutCountBounded :: Int -> Doc () -> Bool
layoutCountBounded k doc = layoutsCountBounded k [doc]
maxLayouts :: Int
maxLayouts = 64
infix 4 `deq`
infix 4 `lseq`
infix 4 `rdeq`
debugRender :: Int -> Doc () -> IO ()
debugRender k = putStr . visibleSpaces . renderStyle (Style PageMode k 1)
visibleSpaces = unlines . map (map visibleSpace) . lines
visibleSpace :: Char -> Char
visibleSpace ' ' = '.'
visibleSpace '.' = error "dot in visibleSpace (avoid confusion, please)"
visibleSpace c = c
-- (1) QuickCheck Properties: Laws
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{-
Monoid laws for <>,<+>,$$,$+$
~~~~~~~~~~~~~
<a,b 1> (x * y) * z = x * (y * z)
<a,b 2> empty * x = x
<a,b 3> x * empty = x
-}
prop_1 op x y z = classify (any isEmpty [x,y,z]) "empty x, y or z" $
((x `op` y) `op` z) `deq` (x `op` (y `op` z))
prop_2 op x = classify (isEmpty x) "empty" $ (empty `op` x) `deq` x
prop_3 op x = classify (isEmpty x) "empty" $ x `deq` (empty `op` x)
check_monoid = do
putStrLn " = Monoid Laws ="
mapM_ (myTest' 5 maxTests "Associativity") [ liftDoc3 (prop_1 op) | op <- allops ]
mapM_ (myTest "Left neutral") [ prop_2 op . buildDoc | op <- allops ]
mapM_ (myTest "Right neutral") [ prop_3 op . buildDoc | op <- allops ]
where
allops = [ (<>), (<+>) ,($$) , ($+$) ]
{-
Laws for text
~~~~~~~~~~~~~
<t1> text s <> text t = text (s++t)
<t2> text "" <> x = x, if x non-empty [only true if x does not start with nest, because of <n6> ]
-}
prop_t1 s t = text' s <> text' t `deq` text (unText s ++ unText t)
prop_t2 x = not (isEmpty x) ==> text "" <> x `deq` x
prop_t2_a x = not (isEmpty x) && not (isNest x) ==> text "" <> x `deq` x
isNest :: Doc () -> Bool
isNest d = case reduceDoc d of
(Nest _ _) -> True
(Union d1 d2) -> isNest d1 || isNest d2
_ -> False
check_t = do
putStrLn " = Text laws ="
myTest "t1" prop_t1
myTest "t2_a (precondition: x does not start with nest)" (prop_t2_a . buildDoc)
myTest "t_2 (Known to fail)" (expectFailure . prop_t2 . buildDoc)
{-
Laws for nest
~~~~~~~~~~~~~
<n1> nest 0 x = x
<n2> nest k (nest k' x) = nest (k+k') x
<n3> nest k (x <> y) = nest k z <> nest k y
<n4> nest k (x $$ y) = nest k x $$ nest k y
<n5> nest k empty = empty
<n6> x <> nest k y = x <> y, if x non-empty
-}
prop_n1 x = nest 0 x `deq` x
prop_n2 k k' x = nest k (nest k' x) `deq` nest (k+k') x
prop_n3 k k' x = nest k (nest k' x) `deq` nest (k+k') x
prop_n4 k x y = nest k (x $$ y) `deq` nest k x $$ nest k y
prop_n5 k = nest k empty `deq` empty
prop_n6 x k y = not (isEmpty x) ==>
x <> nest k y `deq` x <> y
check_n = do
putStrLn "Nest laws"
myTest "n1" (prop_n1 . buildDoc)
myTest "n2" (\k k' -> prop_n2 k k' . buildDoc)
myTest "n3" (\k k' -> prop_n3 k k' . buildDoc)
myTest "n4" (\k -> liftDoc2 (prop_n4 k))
myTest "n5" prop_n5
myTest "n6" (\k -> liftDoc2 (\x -> prop_n6 x k))
{-
<m1> (text s <> x) $$ y = text s <> ((text "" <> x)) $$
nest (-length s) y)
<m2> (x $$ y) <> z = x $$ (y <> z)
if y non-empty
-}
prop_m1 s x y = (text' s <> x) $$ y `deq` text' s <> ((text "" <> x) $$
nest (-length (unText s)) y)
prop_m2 x y z = not (isEmpty y) ==>
(x $$ y) <> z `deq` x $$ (y <> z)
check_m = do
putStrLn "Misc laws"
myTest "m1" (\s -> liftDoc2 (prop_m1 s))
myTest' 10 maxTests "m2" (liftDoc3 prop_m2)
{-
Laws for list versions
~~~~~~~~~~~~~~~~~~~~~~
<l1> sep (ps++[empty]++qs) = sep (ps ++ qs)
...ditto hsep, hcat, vcat, fill...
[ Fails for fill ! ]
<l2> nest k (sep ps) = sep (map (nest k) ps)
...ditto hsep, hcat, vcat, fill...
-}
prop_l1 sp ps qs =
sp (ps++[empty]++qs) `rdeq` sp (ps ++ qs)
prop_l2 sp k ps = nest k (sep ps) `deq` sep (map (nest k) ps)
prop_l1' sp cps cqs =
let [ps,qs] = map buildDocList [cps,cqs] in
layoutCountBounded maxLayouts (sp (ps++qs)) ==> prop_l1 sp ps qs
prop_l2' sp k ps = prop_l2 sp k (buildDocList ps)
check_l = do
allCats $ myTest "l1" . prop_l1'
allCats $ myTest "l2" . prop_l2'
where
allCats = flip mapM_ [ sep, hsep, cat, hcat, vcat, fsep, fcat ]
prop_l1_fail_1 = [ text "a" ]
prop_l1_fail_2 = [ text "a" $$ text "b" ]
{-
Laws for oneLiner
~~~~~~~~~~~~~~~~~
<o1> oneLiner (nest k p) = nest k (oneLiner p)
<o2> oneLiner (x <> y) = oneLiner x <> oneLiner y
[One liner only takes reduced arguments]
-}
oneLinerR = oneLiner . reduceDoc
prop_o1 k p = oneLinerR (nest k p) `deq` nest k (oneLinerR p)
prop_o2 x y = oneLinerR (x <> y) `deq` oneLinerR x <> oneLinerR y
check_o = do
putStrLn "oneliner laws"
myTest "o1 (RDoc arg)" (\k p -> prop_o1 k (buildDoc p))
myTest "o2 (RDoc arg)" (liftDoc2 prop_o2)
{-
Definitions of list versions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<ldef1> vcat = foldr ($$) empty
<ldef2> hcat = foldr (<>) empty
<ldef3> hsep = foldr (<+>) empty
-}
prop_hcat :: [Doc ()] -> Bool
prop_hcat ds = hcat ds `deq` (foldr (<>) empty) ds
prop_hsep :: [Doc ()] -> Bool
prop_hsep ds = hsep ds `deq` (foldr (<+>) empty) ds
prop_vcat :: [Doc ()] -> Bool
prop_vcat ds = vcat ds `deq` (foldr ($$) empty) ds
{-
Update (pretty-1.1.0):
*failing* definition of sep: oneLiner (hsep ps) `union` vcat ps
<ldef4> ?
-}
prop_sep :: [Doc ()] -> Bool
prop_sep ds = sep ds `rdeq` (sepDef ds)
sepDef :: [Doc ()] -> Doc ()
sepDef docs = let ds = filter (not . isEmpty) docs in
case ds of
[] -> empty
[d] -> d
ds -> reduceDoc (oneLiner (reduceDoc $ hsep ds)
`Union`
(reduceDoc $ foldr ($+$) empty ds))
check_list_def = do
myTest "hcat def" (prop_hcat . buildDocList)
myTest "hsep def" (prop_hsep . buildDocList)
myTest "vcat def" (prop_vcat . buildDocList)
-- XXX: Not sure if this is meant to fail (I added the expectFailure [DT])
myTest "sep def" (expectFailure . prop_sep . buildDocList)
{-
Definition of fill (fcat/fsep)
-- Specification:
-- fill [] = empty
-- fill [p] = p
-- fill (p1:p2:ps) = oneLiner p1 <#> nest (length p1)
-- (fill (oneLiner p2 : ps))
-- `union`
-- p1 $$ fill ps
-- Revised Specification:
-- fill g docs = fillIndent 0 docs
--
-- fillIndent k [] = []
-- fillIndent k [p] = p
-- fillIndent k (p1:p2:ps) =
-- oneLiner p1 <g> fillIndent (k + length p1 + g ? 1 : 0) (remove_nests (oneLiner p2) : ps)
-- `Union`
-- (p1 $*$ nest (-k) (fillIndent 0 ps))
--
-- $*$ is defined for layouts (not Docs) as
-- layout1 $*$ layout2 | isOneLiner layout1 = layout1 $+$ layout2
-- | otherwise = layout1 $$ layout2
--
-- Old implementation ambiguities/problems:
-- ========================================
-- Preserving nesting:
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- fcat [cat[ text "b", text "a"], nest 2 ( text "" $$ text "a")]
-- ==> fcat [ text "b" $$ text "a", nest 2 (text "" $$ text "a")] // cat: union right
-- ==> (text "b" $$ text "a" $$ nest 2 (text "" $$ text "a")) // fcat: union right with overlap
-- ==> (text "ab" $$ nest 2 (text "" $$ text "a"))
-- ==> "b\na\n..a"
-- Bug #1337:
-- ~~~~~~~~~~
-- > fcat [ nest 1 $ text "a", nest 2 $ text "b", text "c"]
-- ==> [second alternative] roughly (a <#> b $#$ c)
-- " ab"
-- "c "
-- expected: (nest 1; text "a"; text "b"; nest -3; "c")
-- actual : (nest 1; text "a"; text "b"; nest -5; "c")
-- === (nest 1; text a) <> (fill (-2) (p2:ps))
-- ==> (nest 2 (text "b") $+$ text "c")
-- ==> (nest 2 (text "b") `nilabove` nest (-3) (text "c"))
-- ==> (nest 1; text a; text b; nest -5 c)
-}
prop_fcat_vcat :: [Doc ()] -> Bool
prop_fcat_vcat ds = last (flattenDoc $ fcat ds) `deq` last (flattenDoc $ vcat ds)
prop_fcat :: [Doc ()] -> Bool
prop_fcat ds = fcat ds `rdeq` fillDef False (filter (not . isEmpty) ds)
prop_fsep :: [Doc ()] -> Bool
prop_fsep ds = fsep ds `rdeq` fillDef True (filter (not . isEmpty) ds)
prop_fcat_old :: [Doc ()] -> Bool
prop_fcat_old ds = fillOld2 False ds `rdeq` fillDef False (filter (not . isEmpty) ds)
prop_fcat_old_old :: [Doc ()] -> Bool
prop_fcat_old_old ds = fillOld2 False ds `rdeq` fillDefOld False (filter (not . isEmpty) ds)
prop_restrict_sz :: (Testable a) => Int -> ([Doc ()] -> a) -> ([Doc ()] -> Property)
prop_restrict_sz k p ds = layoutCountBounded k (fsep ds) ==> p ds
prop_restrict_ol :: (Testable a) => ([Doc ()] -> a) -> ([Doc ()] -> Property)
prop_restrict_ol p ds = (all isOneLiner . map normalize $ ds) ==> p ds
prop_restrict_no_nest_start :: (Testable a) => ([Doc ()] -> a) -> ([Doc ()] -> Property)
prop_restrict_no_nest_start p ds = (all (not .isNest) ds) ==> p ds
fillDef :: Bool -> [Doc ()] -> Doc ()
fillDef g = normalize . fill' 0 . filter (not.isEmpty) . map reduceDoc
where
fill' _ [] = Empty
fill' _ [x] = x
fill' k (p1:p2:ps) =
reduceDoc (oneLiner p1 `append` (fill' (k + firstLineLength p1 + (if g then 1 else 0)) $ (oneLiner' p2) : ps))
`union`
reduceDoc (p1 $*$ (nest (-k) (fillDef g (p2:ps))))
union = Union
append = if g then (<+>) else (<>)
oneLiner' (Nest k d) = oneLiner' d
oneLiner' d = oneLiner d
($*$) :: RDoc () -> RDoc () -> RDoc ()
($*$) p ps = case flattenDoc p of
[] -> NoDoc
ls -> foldr1 Union (map combine ls)
where
combine p | isOneLiner p = p $+$ ps
| otherwise = p $$ ps
fillDefOld :: Bool -> [Doc ()] -> Doc ()
fillDefOld g = normalize . fill' . filter (not.isEmpty) . map normalize where
fill' [] = Empty
fill' [p1] = p1
fill' (p1:p2:ps) = (normalize (oneLiner p1 `append` nest (firstLineLength p1)
(fill' (oneLiner p2 : ps))))
`union`
(p1 $$ fill' (p2:ps))
append = if g then (<+>) else (<>)
union = Union
check_fill_prop :: Testable a => String -> ([Doc ()] -> a) -> IO ()
check_fill_prop msg p = myTest msg (prop_restrict_sz maxLayouts p . buildDocList)
check_fill_def_fail :: IO ()
check_fill_def_fail = do
check_fill_prop "fcat defOld vs fcatOld (ol)" (prop_restrict_ol prop_fcat_old_old)
check_fill_prop "fcat defOld vs fcatOld" prop_fcat_old_old
check_fill_prop "fcat def (ol) vs fcatOld" (prop_restrict_ol prop_fcat_old)
check_fill_prop "fcat def vs fcatOld" prop_fcat_old
check_fill_def_ok :: IO ()
check_fill_def_ok = do
check_fill_prop "fcat def (not nest start) vs fcatOld" (prop_restrict_no_nest_start prop_fcat_old)
check_fill_prop "fcat def (not nest start) vs fcat" (prop_restrict_no_nest_start prop_fcat)
-- XXX: These all fail now with the change of pretty to GHC behaviour.
check_fill_prop "fcat def (ol) vs fcat" (expectFailure . prop_restrict_ol prop_fcat)
check_fill_prop "fcat def vs fcat" (expectFailure . prop_fcat)
check_fill_prop "fsep def vs fsep" (expectFailure . prop_fsep)
check_fill_def_laws :: IO ()
check_fill_def_laws = do
check_fill_prop "lastLayout (fcat ps) == vcat ps" prop_fcat_vcat
check_fill_def :: IO ()
check_fill_def = check_fill_def_fail >> check_fill_def_ok
{-
text "ac"; nilabove; nest -1; text "a"; empty
text "ac"; nilabove; nest -2; text "a"; empty
-}
{-
Zero width text (Neil)
Here it would be convenient to generate functions (or replace empty / text bz z-w-t)
-}
-- TODO
{-
All laws: monoid, text, nest, misc, list versions, oneLiner, list def
-}
check_laws :: IO ()
check_laws = do
check_fill_def_ok
check_monoid
check_t
check_n
check_m
check_l
check_o
check_list_def
-- (2) QuickCheck Properties: Invariants (whitebox)
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- strategies: synthesize with stop condition
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
stop :: a -> (a, Bool)
stop a = (a,False)
recurse :: a -> (a, Bool)
recurse a = (a,True)
-- strategy: generic synthesize with stop condition
-- terms are combined top-down, left-right (latin text order)
genericProp :: (a -> a -> a) -> (Doc () -> (a,Bool)) -> Doc () -> a
genericProp c q doc =
case q doc of
(v,False) -> v
(v,True) -> foldl c v (subs doc)
where
rec = genericProp c q
subs d = case d of
Empty -> []
NilAbove d -> [rec d]
TextBeside _ d -> [rec d]
Nest _ d -> [rec d]
Union d1 d2 -> [rec d1, rec d2]
NoDoc -> []
Beside d1 _ d2 -> subs (reduceDoc d)
Above d1 _ d2 -> subs (reduceDoc d)
{-
* The argument of NilAbove is never Empty. Therefore
a NilAbove occupies at least two lines.
-}
prop_inv1 :: Doc () -> Bool
prop_inv1 d = genericProp (&&) nilAboveNotEmpty d where
nilAboveNotEmpty (NilAbove Empty) = stop False
nilAboveNotEmpty _ = recurse True
{-
* The argument of @TextBeside@ is never @Nest@.
-}
prop_inv2 :: Doc () -> Bool
prop_inv2 = genericProp (&&) textBesideNotNest where
textBesideNotNest (TextBeside _ (Nest _ _)) = stop False
textBesideNotNest _ = recurse True
{-
* The layouts of the two arguments of @Union@ both flatten to the same
string
-}
prop_inv3 :: Doc () -> Bool
prop_inv3 = genericProp (&&) unionsFlattenSame where
unionsFlattenSame (Union d1 d2) = stop (pairwiseEqual (extractTexts d1 ++ extractTexts d2))
unionsFlattenSame _ = recurse True
pairwiseEqual (x:y:zs) = x==y && pairwiseEqual (y:zs)
pairwiseEqual _ = True
{-
* The arguments of @Union@ are either @TextBeside@, or @NilAbove@.
-}
prop_inv4 :: Doc () -> Bool
prop_inv4 = genericProp (&&) unionArgs where
unionArgs (Union d1 d2) | goodUnionArg d1 && goodUnionArg d2 = recurse True
| otherwise = stop False
unionArgs _ = recurse True
goodUnionArg (TextBeside _ _) = True
goodUnionArg (NilAbove _) = True
goodUnionArg _ = False
{-
* A @NoDoc@ may only appear on the first line of the left argument of
an union. Therefore, the right argument of an union can never be equivalent
to the empty set.
-}
prop_inv5 :: Doc () -> Bool
prop_inv5 = genericProp (&&) unionArgs . reduceDoc where
unionArgs NoDoc = stop False
unionArgs (Union d1 d2) = stop $ genericProp (&&) noDocIsFirstLine d1 && nonEmptySet (reduceDoc d2)
unionArgs _ = (True,True) -- recurse
noDocIsFirstLine (NilAbove d) = stop $ genericProp (&&) unionArgs d
noDocIsFirstLine _ = recurse True
{-
* An empty document is always represented by @Empty@. It can't be
hidden inside a @Nest@, or a @Union@ of two @Empty@s.
-}
prop_inv6 :: Doc () -> Bool
prop_inv6 d | not (prop_inv1 d) || not (prop_inv2 d) = False
| not (isEmptyDoc d) = True
| otherwise = case d of Empty -> True ; _ -> False
isEmptyDoc :: Doc () -> Bool
isEmptyDoc d = case emptyReduction d of Empty -> True; _ -> False
{-
* Consistency
If all arguments of one of the list versions are empty documents, the list is an empty document
-}
prop_inv6a :: ([Doc ()] -> Doc ()) -> Property
prop_inv6a sep = forAll emptyDocListGen $
\ds -> isEmptyRepr (sep $ buildDocList ds)
where
isEmptyRepr Empty = True
isEmptyRepr _ = False
{-
* The first line of every layout in the left argument of @Union@ is
longer than the first line of any layout in the right argument.
(1) ensures that the left argument has a first line. In view of
(3), this invariant means that the right argument must have at
least two lines.
-}
counterexample_inv7 = cat [ text " ", nest 2 ( text "a") ]
prop_inv7 :: Doc () -> Bool
prop_inv7 = genericProp (&&) firstLonger where
firstLonger (Union d1 d2) = (firstLineLength d1 >= firstLineLength d2, True)
firstLonger _ = (True, True)
{-
* If we take as precondition: the arguments of cat,sep,fill do not start with Nest, invariant 7 holds
-}
prop_inv7_pre :: CDoc -> Bool
prop_inv7_pre cdoc = nestStart True cdoc where
nestStart nestOk doc =
case doc of
CList sep ds -> all (nestStart False) ds
CBeside _ d1 d2 -> nestStart nestOk d1 && nestStart (not . isEmpty $ buildDoc d1) d2
CAbove _ d1 d2 -> nestStart nestOk d1 && nestStart (not . isEmpty $ buildDoc d1) d2
CNest _ d | not nestOk -> False
| otherwise -> nestStart True d
_empty_or_text -> True
{-
inv7_pre ==> inv7
-}
prop_inv7_a :: CDoc -> Property
prop_inv7_a cdoc = prop_inv7_pre cdoc ==> prop_inv7 (buildDoc cdoc)
check_invariants :: IO ()
check_invariants = do
myTest "Invariant 1" (prop_inv1 . buildDoc)
myTest "Invariant 2" (prop_inv2 . buildDoc)
myTest "Invariant 3" (prop_inv3 . buildDoc)
myTest "Invariant 4" (prop_inv4 . buildDoc)
myTest "Invariant 5+" (prop_inv5 . buildDoc)
myTest "Invariant 6" (prop_inv6 . buildDoc)
mapM_ (\sp -> myTest "Invariant 6a" $ prop_inv6a sp) [ cat, sep, fcat, fsep, vcat, hcat, hsep ]
-- XXX: Not sure if this is meant to fail (I added the expectFailure [DT])
myTest "Invariant 7 (fails in HughesPJ:20080621)" (expectFailure . prop_inv7 . buildDoc)
-- `negative indent'
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{-
In the documentation we have:
(spaces n) generates a list of n spaces
It should never be called with 'n' < 0, but that can happen for reasons I don't understand
This is easy to explain:
Suppose we have layout1 <> layout2
length of last line layout1 is k1
indentation of first line of layout2 is k2
indentation of some other line of layout2 is k2'
Now layout1 <> nest k2 (line1 $$ nest k2' lineK)
==> layout1 <> (line1 $$ nest k2' lineK)
When k1 - k2' < 0, we need to layout lineK with negative indentation
Here is a quick check property to ducment this.
-}
prop_negative_indent :: CDoc -> Property
prop_negative_indent cdoc = noNegNest cdoc ==> noNegSpaces (buildDoc cdoc)
noNegNest = genericCProp (&&) notIsNegNest where
notIsNegNest (CNest k _) | k < 0 = stop False
notIsNegNest _ = recurse True
noNegSpaces = go 0 . reduceDoc where
go k Empty = True
go k (NilAbove d) = go k d
go k (TextBeside _ d) | k < 0 = False
go k (TextBeside s d) = go (k+annotSize s) d
go k (Nest k' d) = go (k+k') d
go k (Union d1 d2) = (if nonEmptySet d1 then (&&) (go k d1) else id) (go k d2)
go k NoDoc = True
counterexample_fail9 :: Doc ()
counterexample_fail9 = text "a" <> ( nest 2 ( text "b") $$ text "c")
-- reduces to textb "a" ; textb "b" ; nilabove ; nest -3 ; textb "c" ; empty
{-
This cannot be fixed with violating the "intuitive property of layouts", described by John Hughes:
"Composing layouts should preserve the layouts themselves (i.e. translation)"
Consider the following example:
It is the user's fault to use <+> in t2.
-}
tstmt = (nest 6 $ text "/* double indented comment */") $+$
(nest 3 $ text "/* indented comment */") $+$
text "skip;"
t1 = text "while(true)" $+$ (nest 2) tstmt
{-
while(true)
/* double indented comment */
/* indented comment */
skip;
-}
t2 = text "while(true)" $+$ (nest 2 $ text "//" <+> tstmt)
{-
while(true)
// /* double indented comment */
/* indented comment */
skip;
-}
-- (3) Touching non-prims
-- ~~~~~~~~~~~~~~~~~~~~~~
check_non_prims :: IO ()
check_non_prims = do
myTest "Non primitive: show = renderStyle style" $ \cd -> let d = buildDoc cd in
show ((zeroWidthText "a") <> d) /= renderStyle style d
myAssert "symbols" $
(semi <> comma <> colon <> equals <> lparen <> rparen <> lbrack <> rbrack <> lbrace <> rbrace)
`deq`
(text ";,:=()[]{}")
myAssert "quoting" $
(quotes . doubleQuotes . parens . brackets .braces $ (text "a" $$ text "b"))
`deq`
(text "'\"([{" <> (text "a" $$ text "b") <> text "}])\"'")
myAssert "numbers" $
fsep [int 42, integer 42, float 42, double 42, rational 42]
`rdeq`
(fsep . map text)
[show (42 :: Int), show (42 :: Integer), show (42 :: Float), show (42 :: Double), show (42 :: Rational)]
myTest "Definition of <+>" $ \cd1 cd2 ->
let (d1,d2) = (buildDoc cd1, buildDoc cd2) in
layoutsCountBounded maxLayouts [d1,d2] ==>
not (isEmpty d1) && not (isEmpty d2) ==>
d1 <+> d2 `rdeq` d1 <> space <> d2
myTest "hang" $ liftDoc2 (\d1 d2 -> hang d1 2 d2 `deq` sep [d1, nest 2 d2])
let pLift f cp cds = f (buildDoc cp) (buildDocList cds)
myTest "punctuate" $ pLift (\p ds -> (punctuate p ds) `deqs` (punctuateDef p ds))
check_rendering = do
myTest' 20 10000 "one - line rendering" $ \cd ->
let d = buildDoc cd in
(renderStyle (Style OneLineMode undefined undefined) d) == oneLineRender d
myTest' 20 10000 "left-mode rendering" $ \cd ->
let d = buildDoc cd in
extractText (renderStyle (Style LeftMode undefined undefined) d) == extractText (oneLineRender d)
myTest' 20 10000 "page mode rendering" $ \cd ->
let d = buildDoc cd in
extractText (renderStyle (Style PageMode 6 1.7) d) == extractText (oneLineRender d)
myTest' 20 10000 "zigzag mode rendering" $ \cd ->
let d = buildDoc cd in
extractTextZZ (renderStyle (Style ZigZagMode 6 1.7) d) == extractText (oneLineRender d)
extractText :: String -> String
extractText = filter (not . isSpace)
extractTextZZ :: String -> String
extractTextZZ = filter (\c -> not (isSpace c) && c /= '/' && c /= '\\')
punctuateDef :: Doc () -> [Doc ()] -> [Doc ()]
punctuateDef p [] = []
punctuateDef p ps =
let (dsInit,dLast) = (init ps, last ps) in
map (\d -> d <> p) dsInit ++ [dLast]
-- (4) QuickChecking improvments and bug fixes
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{-
putStrLn $ render' $ fill True [ text "c", text "c",empty, text "c", text "b"]
c c c
b
putStrLn $ render' $ fillOld True [ text "c", text "c",empty, text "c", text "b"]
c c c
b
-}
prop_fill_empty_reduce :: [Doc ()] -> Bool
prop_fill_empty_reduce ds = fill True ds `deq` fillOld True (filter (not.isEmpty.reduceDoc) ds)
check_improvements :: IO ()
check_improvements = do
myTest "fill = fillOld . filter (not.isEmpty) [if no argument starts with nest]"
(prop_fill_empty_reduce . filter (not .isNest) . buildDocList)
-- old implementation of fill
fillOld :: Bool -> [Doc ()] -> RDoc ()
fillOld _ [] = empty
fillOld g (p:ps) = fill1 g (reduceDoc p) 0 ps where
fill1 :: Bool -> RDoc () -> Int -> [Doc ()] -> Doc ()
fill1 _ _ k _ | k `seq` False = undefined
fill1 _ NoDoc _ _ = NoDoc
fill1 g (p `Union` q) k ys = fill1 g p k ys
`union_`
(aboveNest q False k (fillOld g ys))
fill1 g Empty k ys = mkNest k (fillOld g ys)
fill1 g (Nest n p) k ys = nest_ n (fill1 g p (k - n) ys)
fill1 g (NilAbove p) k ys = nilAbove_ (aboveNest p False k (fillOld g ys))
fill1 g (TextBeside s p) k ys = textBeside_ s (fillNB g p (k - annotSize s) ys)
fill1 _ (Above {}) _ _ = error "fill1 Above"
fill1 _ (Beside {}) _ _ = error "fill1 Beside"
-- fillNB gap textBesideArgument space_left docs
fillNB :: Bool -> Doc () -> Int -> [Doc ()] -> Doc ()
fillNB _ _ k _ | k `seq` False = undefined
fillNB g (Nest _ p) k ys = fillNB g p k ys
fillNB _ Empty _ [] = Empty
fillNB g Empty k (y:ys) = nilBeside g (fill1 g (oneLiner (reduceDoc y)) k1 ys)
`mkUnion`
nilAboveNest False k (fillOld g (y:ys))
where
k1 | g = k - 1
| otherwise = k
fillNB g p k ys = fill1 g p k ys
-- Specification:
-- fill [] = empty
-- fill [p] = p
-- fill (p1:p2:ps) = oneLiner p1 <#> nest (length p1)
-- (fill (oneLiner p2 : ps))
-- `union`
-- p1 $$ fill ps
fillOld2 :: Bool -> [Doc ()] -> RDoc ()
fillOld2 _ [] = empty
fillOld2 g (p:ps) = fill1 g (reduceDoc p) 0 ps where
fill1 :: Bool -> RDoc () -> Int -> [Doc ()] -> Doc ()
fill1 _ _ k _ | k `seq` False = undefined
fill1 _ NoDoc _ _ = NoDoc
fill1 g (p `Union` q) k ys = fill1 g p k ys
`union_`
(aboveNest q False k (fill g ys))
fill1 g Empty k ys = mkNest k (fill g ys)
fill1 g (Nest n p) k ys = nest_ n (fill1 g p (k - n) ys)
fill1 g (NilAbove p) k ys = nilAbove_ (aboveNest p False k (fill g ys))
fill1 g (TextBeside s p) k ys = textBeside_ s (fillNB g p (k - annotSize s) ys)
fill1 _ (Above {}) _ _ = error "fill1 Above"
fill1 _ (Beside {}) _ _ = error "fill1 Beside"
fillNB :: Bool -> Doc () -> Int -> [Doc ()] -> Doc ()
fillNB _ _ k _ | k `seq` False = undefined
fillNB g (Nest _ p) k ys = fillNB g p k ys
fillNB _ Empty _ [] = Empty
fillNB g Empty k (Empty:ys) = fillNB g Empty k ys
fillNB g Empty k (y:ys) = fillNBE g k y ys
fillNB g p k ys = fill1 g p k ys
fillNBE g k y ys = nilBeside g (fill1 g (oneLiner (reduceDoc y)) k1 ys)
`mkUnion`
nilAboveNest True k (fill g (y:ys))
where
k1 | g = k - 1
| otherwise = k
-- (5) Pretty printing RDocs and RDOC properties
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
prettyDoc :: Doc () -> Doc ()
prettyDoc d =
case reduceDoc d of
Empty -> text "empty"
NilAbove d -> (text "nilabove") <> semi <+> (prettyDoc d)
TextBeside s d -> (text ("text \""++tdToStr (annotToTd s) ++ "\"" ++ show (annotSize s))) <> semi <+> (prettyDoc d)
Nest k d -> text "nest" <+> integer (fromIntegral k) <> semi <+> prettyDoc d
Union d1 d2 -> sep [text "union", parens (prettyDoc d1), parens (prettyDoc d2)]
NoDoc -> text "nodoc"
-- TODO: map strategy for Docs to avoid code duplication
-- Debug: Doc -> [Layout]
flattenDoc :: Doc () -> [RDoc ()]
flattenDoc d = flatten (reduceDoc d) where
flatten NoDoc = []
flatten Empty = return Empty
flatten (NilAbove d) = map NilAbove (flatten d)
flatten (TextBeside s d) = map (TextBeside s) (flatten d)
flatten (Nest k d) = map (Nest k) (flatten d)
flatten (Union d1 d2) = flattenDoc d1 ++ flattenDoc d2
flatten (Beside d1 b d2) = error $ "flattenDoc Beside"
flatten (Above d1 b d2) = error $ "flattenDoc Above"
normalize :: Doc () -> RDoc ()
normalize d = norm d where
norm NoDoc = NoDoc
norm Empty = Empty
norm (NilAbove d) = NilAbove (norm d)
norm (TextBeside s (Nest k d)) = norm (TextBeside s d)
norm (TextBeside s d) = (TextBeside s) (norm d)
norm (Nest k (Nest k' d)) = norm $ Nest (k+k') d
norm (Nest 0 d) = norm d
norm (Nest k d) = (Nest k) (norm d)
-- * The arguments of @Union@ are either @TextBeside@, or @NilAbove@.
norm (Union d1 d2) = normUnion (norm d1) (norm d2)
norm d@(Beside d1 b d2) = norm (reduceDoc d)
norm d@(Above d1 b d2) = norm (reduceDoc d)
normUnion d0@(Nest k d) (Union d1 d2) = norm (Union d0 (normUnion d1 d2))
normUnion (Union d1 d2) d3@(Nest k d) = norm (Union (normUnion d1 d2) d3)
normUnion (Nest k d1) (Nest k' d2) | k == k' = Nest k $ Union (norm d1) (norm d2)
| otherwise = error "normalize: Union Nest length mismatch ?"
normUnion (Nest _ _) d2 = error$ "normUnion Nest "++topLevelCTor d2
normUnion d1 (Nest _ _) = error$ "normUnion Nset "++topLevelCTor d1
normUnion p1 p2 = Union p1 p2
topLevelCTor :: Doc () -> String
topLevelCTor d = tlc d where
tlc NoDoc = "NoDoc"
tlc Empty = "Empty"
tlc (NilAbove d) = "NilAbove"
tlc (TextBeside s d) = "TextBeside"
tlc (Nest k d) = "Nest"
tlc (Union d1 d2) = "Union"
tlc (Above _ _ _) = "Above"
tlc (Beside _ _ _) = "Beside"
-- normalize TextBeside (and consequently apply some laws for simplification)
mergeTexts :: RDoc () -> RDoc ()
mergeTexts = merge where
merge NoDoc = NoDoc
merge Empty = Empty
merge (NilAbove d) = NilAbove (merge d)
merge (TextBeside t1 (TextBeside t2 doc)) = (merge.normalize) (TextBeside (mergeText t1 t2) doc)
merge (TextBeside s d) = TextBeside s (merge d)
merge (Nest k d) = Nest k (merge d)
merge (Union d1 d2) = Union (merge d1) (merge d2)
mergeText t1 t2 =
NoAnnot (Str $ tdToStr (annotToTd t1) ++ tdToStr (annotToTd t2))
(annotSize t1 + annotSize t2)
isOneLiner :: RDoc () -> Bool
isOneLiner = genericProp (&&) iol where
iol (NilAbove _) = stop False
iol (Union _ _) = stop False
iol NoDoc = stop False
iol _ = recurse True
hasOneLiner :: RDoc () -> Bool
hasOneLiner = genericProp (&&) iol where
iol (NilAbove _) = stop False
iol (Union d1 _) = stop $ hasOneLiner d1
iol NoDoc = stop False
iol _ = recurse True
-- use elementwise concatenation as generic combinator
extractTexts :: Doc () -> [String]
extractTexts = map normWS . genericProp combine go where
combine xs ys = [ a ++ b | a <- xs, b <- ys ]
go (TextBeside s _ ) = recurse [tdToStr (annotToTd s)]
go (Union d1 d2) = stop $ extractTexts d1 ++ extractTexts d2
go NoDoc = stop []
go _ = recurse [""]
-- modulo whitespace
normWS txt = filter (not . isWS) txt where
isWS ws | ws == ' ' || ws == '\n' || ws == '\t' = True
| otherwise = False
emptyReduction :: Doc () -> Doc ()
emptyReduction doc =
case doc of
Empty -> Empty
NilAbove d -> case emptyReduction d of Empty -> Empty ; d' -> NilAbove d'
TextBeside s d -> TextBeside s (emptyReduction d)
Nest k d -> case emptyReduction d of Empty -> Empty; d -> Nest k d
Union d1 d2 -> case emptyReduction d2 of Empty -> Empty; _ -> Union d1 d2 -- if d2 is empty, both have to be
NoDoc -> NoDoc
Beside d1 _ d2 -> emptyReduction (reduceDoc doc)
Above d1 _ d2 -> emptyReduction (reduceDoc doc)
firstLineLength :: Doc () -> Int
firstLineLength = genericProp (+) fll . reduceDoc where
fll (NilAbove d) = stop 0
fll (TextBeside s d) = recurse (annotSize s)
fll (Nest k d) = recurse k
fll (Union d1 d2) = stop (firstLineLength d1) -- inductively assuming inv7
fll (Above _ _ _) = error "Above"
fll (Beside _ _ _) = error "Beside"
fll _ = (0,True)
abstractLayout :: Doc () -> [(Int,String)]
abstractLayout d = cal 0 Nothing (reduceDoc d) where
-- current column -> this line -> doc -> [(indent,line)]
cal :: Int -> (Maybe (Int,String)) -> Doc () -> [(Int,String)]
cal k cur Empty = [ addTextEOL k (Str "") cur ]
cal k cur (NilAbove d) = (addTextEOL k (Str "") cur) : cal k Nothing d
cal k cur (TextBeside s d) = cal (k + annotSize s) (addText k s cur) d
cal k cur (Nest n d) = cal (k+n) cur d
cal _ _ (Union d1 d2) = error "abstractLayout: Union"
cal _ _ NoDoc = error "NoDoc"
cal _ _ (Above _ _ _) = error "Above"
cal _ _ (Beside _ _ _) = error "Beside"
addTextEOL k str Nothing = (k,tdToStr str)
addTextEOL _ str (Just (k,pre)) = (k,pre++ tdToStr str)
addText k str = Just . addTextEOL k (annotToTd str)
docifyLayout :: [(Int,String)] -> Doc ()
docifyLayout = vcat . map (\(k,t) -> nest k (text t))
oneLineRender :: Doc () -> String
oneLineRender = olr . abstractLayout . last . flattenDoc where
olr = concat . intersperse " " . map snd
-- because of invariant 4, we do not have to expand to layouts here
-- but it is easier, so for now we use abstractLayout
firstLineIsLeftMost :: Doc () -> Bool
firstLineIsLeftMost = all (firstIsLeftMost . abstractLayout) . flattenDoc where
firstIsLeftMost ((k,_):xs@(_:_)) = all ( (>= k) . fst) xs
firstIsLeftMost _ = True
noNegativeIndent :: Doc () -> Bool
noNegativeIndent = all (noNegIndent . abstractLayout) . flattenDoc where
noNegIndent = all ( (>= 0) . fst)
|