-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTesting.hs
307 lines (257 loc) · 10.5 KB
/
Testing.hs
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
{-# LANGUAGE FlexibleInstances #-}
module Testing where
import qualified Data.Map as Map
import Data.Either (isRight)
import Test.HUnit hiding (Testable)
import Test.QuickCheck
import Debug.Trace
import Language
import Parser
import Builtin
import Inference
import Main
quickCheckN :: Testable prop => Int -> prop -> IO ()
quickCheckN n = quickCheckWith $ stdArgs { maxSuccess = n }
main :: IO ()
main =
do _ <- runTestTT $ TestList [testParser, testTypes, testInterpret]
quickCheckN 10000 prop_id
quickCheckN 10000 prop_quote
quickCheckN 10000 prop_associative
quickCheckN 1000 prop_concat
quickCheckN 10000 prop_wellTyped
testParser :: Test
testParser = TestList [testParseEmpty, testParseBuiltin, testParseQuotes]
testTypes :: Test
testTypes = TestList [testTypesBase, testTypesBuiltin,
testTypesQuotes, testTypesConcat]
testInterpret :: Test
testInterpret = TestList [testInterpretBase, testInterpretConcat]
-- Tests for parser
parsesTo :: String -> Term () -> Test
parsesTo s t =
case parse s of
Right t' -> t ~=? t'
Left err -> False ~? show err
noParse :: String -> Test
noParse s =
case parse s of
Right _ -> False ~? "Should not parse: " ++ s
Left _ -> True ~? "Failed as expected"
testParseEmpty :: Test
testParseEmpty = "Parsing whitespace and no tokens" ~: TestList
[ "Empty string" ~: "" `parsesTo` IdTerm ()
, "Whitespace chars" ~: " " `parsesTo` IdTerm ()
, "Lots of whitespace" ~: " " `parsesTo` IdTerm ()
, "Comment" ~: "(* Hi *)" `parsesTo` IdTerm ()
]
testParseLiteral :: Test
testParseLiteral = "Parsing literal bool and int values" ~: TestList
[ "Single digit numbers" ~: "1" `parsesTo` PushIntTerm () 1
, "Multi-digit numbers" ~: "552" `parsesTo` PushIntTerm () 552
, "Negative numbers" ~: "-12" `parsesTo` PushIntTerm () (-12)
, "True Boolean" ~: "true" `parsesTo` PushBoolTerm () True
, "False Boolean" ~: "false" `parsesTo` PushBoolTerm () False
, "Int with whitespace" ~: " 10 " `parsesTo` PushIntTerm () 10
]
testParseBuiltin :: Test
testParseBuiltin = "Parsing (presumed builtin) identifiers" ~: TestList
[ "Single letter" ~: "a" `parsesTo` BuiltinTerm () "a"
, "Single word" ~: "foo" `parsesTo` BuiltinTerm () "foo"
, "Word with whitespace" ~: " foo " `parsesTo` BuiltinTerm () "foo"
, "Word with underscore" ~: "ok_ay" `parsesTo` BuiltinTerm () "ok_ay"
]
testParseQuotes :: Test
testParseQuotes = "Parsing quotations (first-class functions)" ~: TestList
[ "Empty quotation" ~: "{ }" `parsesTo` PushFuncTerm () (IdTerm ())
, "Empty quot no spaces" ~: "{}" `parsesTo` PushFuncTerm () (IdTerm ())
, "Quoted builtin" ~: "{foo}" `parsesTo`
PushFuncTerm () (BuiltinTerm () "foo")
, "Quoted and spaces" ~: "{ foo }" `parsesTo`
PushFuncTerm () (BuiltinTerm () "foo")
, "Many quoted names" ~: "{ f b }" `parsesTo`
PushFuncTerm () (CatTerm () (BuiltinTerm () "f") (BuiltinTerm () "b"))
, "Nested quotes" ~: "{{ }}" `parsesTo`
PushFuncTerm () (PushFuncTerm () $ IdTerm ())
, "Mismatched brackets" ~: noParse "{ foo"
]
-- Tests for type inference
-- Variable-independent comparison of two FuncTypes
infix 4 ~:~
(~:~) :: FuncType -> FuncType -> Bool
f ~:~ g = fty == gty
where Right (fty, _) = runTC $ freshen f
Right (gty, _) = runTC $ freshen g
hasType :: Term () -> FuncType -> Test
hasType term ty =
case extract <$> typeInference term of
Right ty' -> ty ~:~ ty' ~? show ty ++ "\n" ++ show ty'
Left err -> False ~? show err
hasNoType :: Term () -> Test
hasNoType term =
case typeInference term of
Right _ -> False ~? "Should not typecheck: " ++ show term
Left err -> True ~? "Failed as expected"
int, bool, idType, plusType :: ValueType
int = VIntTy
bool = VBoolTy
idType = VFuncTy $ F (S "A" []) (S "A" [])
plusType = VFuncTy $ F (S "A" [int, int]) (S "A" [int])
pushes :: [ValueType] -> FuncType
pushes l = F (S "A" []) (S "A" $ go l)
where go [] = []
go (VFuncTy f : ts) = VFuncTy ty' : go ts
where Right (ty', _) = runTC $ freshen f
go (t : ts) = t : go ts
testTypesBase :: Test
testTypesBase = "Type inference for base types" ~: TestList
[ PushIntTerm () 0 `hasType` pushes [int]
, PushIntTerm () 100 `hasType` pushes [int]
, PushBoolTerm () True `hasType` pushes [bool]
, PushBoolTerm () False `hasType` pushes [bool]
]
testTypesBuiltin :: Test
testTypesBuiltin = "Type inference for builtin functions" ~: TestList
[ BuiltinTerm () "plus" `hasType` F (S "A" [int, int]) (S "A" [int])
, BuiltinTerm () "minus" `hasType` F (S "A" [int, int]) (S "A" [int])
, BuiltinTerm () "times" `hasType` F (S "A" [int, int]) (S "A" [int])
, hasNoType $ BuiltinTerm () "not_a_builtin"
]
testTypesQuotes :: Test
testTypesQuotes = "Type inference for quotations" ~: TestList
[ PushFuncTerm () (IdTerm ()) `hasType` pushes [idType]
, PushFuncTerm () (BuiltinTerm () "plus") `hasType` pushes [plusType]
, PushFuncTerm () (PushIntTerm () 3) `hasType`
pushes [VFuncTy $ pushes [int]]
, PushFuncTerm () (PushBoolTerm () False) `hasType`
pushes [VFuncTy $ pushes [bool]]
, PushFuncTerm () (PushFuncTerm () $ IdTerm ()) `hasType`
pushes [VFuncTy $ pushes [idType]]
, PushFuncTerm () (PushFuncTerm () (PushIntTerm () 3)) `hasType`
pushes [VFuncTy $ pushes [VFuncTy $ pushes [int]]]
]
testTypesConcat :: Test
testTypesConcat = "Type inference for term concatentation" ~: TestList
[ CatTerm () (IdTerm ()) (IdTerm ()) `hasType` pushes []
, CatTerm () (IdTerm ()) (PushIntTerm () 3) `hasType` pushes [int]
, CatTerm () (PushIntTerm () 3) (PushIntTerm () 2) `hasType` pushes [int, int]
, CatTerm () (PushIntTerm () 3)
(CatTerm () (PushBoolTerm () False) (PushBoolTerm () True))
`hasType` pushes [int, bool, bool]
, CatTerm () (CatTerm () (PushIntTerm () 3) (PushBoolTerm () False))
(PushBoolTerm () True)
`hasType` pushes [int, bool, bool]
]
-- Tests for interpreter
evalsTo :: Term () -> [Value] -> Test
term `evalsTo` expectedValues =
case typeInference term of
Right typedTerm ->
let actualValues = reverse $ interpret typedTerm [] in
(length expectedValues == length actualValues
&& valuesEqual actualValues expectedValues) ~?= True
Left _ -> False ~? "Should be equal"
testInterpretBase :: Test
testInterpretBase = "Interpret base values" ~: TestList
[ PushIntTerm () 0 `evalsTo` [IntVal 0]
, PushIntTerm () 100 `evalsTo` [IntVal 100]
, PushBoolTerm () False `evalsTo` [BoolVal False]
, PushBoolTerm () True `evalsTo` [BoolVal True]
]
push3, push5, pushf :: Term ()
push3 = PushIntTerm () 3
push5 = PushIntTerm () 5
pusht = PushBoolTerm () True
pushf = PushBoolTerm () False
testInterpretConcat :: Test
testInterpretConcat = "Interpret term concatentation" ~: TestList
[ CatTerm () (IdTerm ()) (IdTerm ()) `evalsTo` []
, CatTerm () (IdTerm ()) push5 `evalsTo` [IntVal 5]
, CatTerm () push5 push3 `evalsTo` [IntVal 5, IntVal 3]
, CatTerm () pushf push3 `evalsTo` [BoolVal False, IntVal 3]
, CatTerm () (CatTerm () push5 push3) push5 `evalsTo`
[IntVal 5, IntVal 3, IntVal 5]
, CatTerm () (CatTerm () push3 pushf) (CatTerm () pusht push5) `evalsTo`
[IntVal 3, BoolVal False, BoolVal True, IntVal 5]
, CatTerm () (CatTerm () (CatTerm () push3 push5) push3) pusht `evalsTo`
[IntVal 3, IntVal 5, IntVal 3, BoolVal True]
, CatTerm () (CatTerm () push3 push5)
(CatTerm () (PushFuncTerm () (BuiltinTerm () "plus"))
(BuiltinTerm () "apply2to1"))
`evalsTo` [IntVal 8]
]
-- QuickCheck properties for type inference
instance Arbitrary (Term ()) where
arbitrary = frequency
[ (1, return $ IdTerm ())
, (8, CatTerm () <$> arbitrary <*> arbitrary)
, (4, BuiltinTerm () <$> elements (Map.keys builtins))
, (4, PushIntTerm () <$> arbitrary)
, (2, PushBoolTerm () <$> arbitrary)
, (5, PushFuncTerm () <$> arbitrary)
]
shrink (CatTerm () t1 t2) = [t1, t2]
shrink (PushFuncTerm () t) = [t]
shrink _ = []
prop_id :: Term () -> Bool
prop_id term =
case extract <$> typeInference term of
Right ty ->
case (extract <$> typeInference cat1, extract <$> typeInference cat2) of
(Right ty', Right ty'') -> ty ~:~ ty' && ty ~:~ ty''
_ -> False
Left _ -> discard
where cat1 = CatTerm () (IdTerm ()) term
cat2 = CatTerm () term (IdTerm ())
prop_quote :: Term () -> Bool
prop_quote term =
case extract <$> typeInference term of
Right ty ->
case extract <$> typeInference (PushFuncTerm () term) of
Right ty' -> ty' ~:~ pushes [VFuncTy ty]
Left _ -> False
Left _ -> discard
prop_associative :: Term () -> Term () -> Term () -> Bool
prop_associative term1 term2 term3 =
case (extract <$> typeInference cat1, extract <$> typeInference cat2) of
(Right ty1, Right ty2) -> ty1 ~:~ ty2
(Left _, Left _) -> True
_ -> False
where cat1 = CatTerm () (CatTerm () term1 term2) term3
cat2 = CatTerm () term1 (CatTerm () term2 term3)
prop_concat :: Term () -> Term () -> Bool
prop_concat term1 term2 =
let ty1 = extract <$> typeInferenceOnEmpty term1
ty2 = extract <$> typeInferenceOnEmpty term2
in case (ty1, ty2) of
(Right (F a b), Right (F c d)) ->
if b == c then
case extract <$> typeInferenceOnEmpty (CatTerm () term1 term2) of
Right ty' -> ty' ~:~ F a d
Left _ -> False
else discard
_ -> discard
prop_wellTyped :: Term () -> Bool
prop_wellTyped term =
case typeInferenceOnEmpty term of
Right typedTerm ->
let F _ (S _ exp) = extract typedTerm
resultStack = reverse $ interpret typedTerm []
resStackTy = stackType resultStack
resFunc = F (S "" []) resStackTy
in F (S "" []) (S "" exp) ~:~ resFunc
Left _ -> discard
stackType :: [Value] -> Stack
stackType = S "" . map valueType
valueType :: Value -> ValueType
valueType (IntVal _) = VIntTy
valueType (BoolVal _) = VBoolTy
valueType (ListVal t _) = VListTy t
valueType (FuncVal t _) = VFuncTy t
valueEqual :: Value -> Value -> Bool
valueEqual (IntVal i1) (IntVal i2) = i1 == i2
valueEqual (BoolVal b1) (BoolVal b2) = b1 == b2
valueEqual (ListVal _ l1) (ListVal _ l2) = valuesEqual l1 l2
valueEqual (FuncVal _ _) (FuncVal _ _) = error "Can't equate functions"
valuesEqual :: [Value] -> [Value] -> Bool
valuesEqual = (and .) . zipWith valueEqual