File size: 11,351 Bytes
a10b189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
{-# LANGUAGE OverloadedStrings #-}

{-|
Module      : Test.Core.Termination
Description : Tests for termination checking (Agent 4B)
-}

module Test.Core.Termination
  ( tests
  ) where

import Test.Tasty
import Test.Tasty.HUnit
import qualified Data.Text as T

import Assertica.Core.AST
import Assertica.Core.Termination
import Assertica.Core.TypeEnv

-- ============================================================================
-- TEST SUITE
-- ============================================================================

tests :: TestTree
tests = testGroup "Assertica.Core.Termination"
  [ testGroup "Recursion Detection"
      [ testCase "No recursion detected in simple term" testNoRecursion
      , testCase "Direct recursion detected" testDirectRecursion
      , testCase "Recursion in application" testRecursionInApp
      , testCase "Recursion in case expression" testRecursionInCase
      ]

  , testGroup "Simple Recursion Patterns"
      [ testCase "Simple structural recursion accepted" testStructuralRecursion
      , testCase "Non-terminating recursion rejected" testNonTerminating
      , testCase "Guarded recursion accepted" testGuardedRecursion
      ]

  , testGroup "Recursive Call Analysis"
      [ testCase "Find direct recursive call" testFindDirectCall
      , testCase "Analyze structural call pattern" testStructuralCallPattern
      , testCase "Detect unsupported recursion pattern" testUnsupportedPattern
      ]

  , testGroup "Mutual Recursion"
      [ testCase "Mutual recursion with progress" testMutualRecursionProgress
      , testCase "Infinite mutual recursion rejected" testInfiniteMutualRecursion
      , testCase "Call cycle detection" testCallCycleDetection
      ]

  , testGroup "Integration Tests"
      [ testCase "Length function on List" testLengthFunction
      , testCase "Fibonacci with guards" testFibonacciFunction
      , testCase "Map over list" testMapFunction
      ]

  , testGroup "Error Messages"
      [ testCase "Non-terminating error message" testNonTerminatingError
      , testCase "Unsupported pattern error message" testUnsupportedPatternError
      , testCase "Cycle detection error message" testCycleDetectionError
      ]
  ]

-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================

{-| Create a simple variable. -}
var :: Text -> Var
var name = Var name 0

{-| Create a simple qualified name. -}
qn :: Text -> QName
qn name = QName [] name

{-| Create a binder. -}
binder :: Text -> Maybe Type -> Binder
binder name mty = Binder (var name) mty

{-| Create a pattern for a variable. -}
patVar :: Text -> Pattern
patVar name = PatVar (var name)

{-| Create a clause. -}
clause :: Pattern -> Term -> Clause
clause = Clause

-- ============================================================================
-- RECURSION DETECTION TESTS
-- ============================================================================

testNoRecursion :: Assertion
testNoRecursion = do
  -- A simple non-recursive term
  let term = Const (IntLit 42)
  assertBool "Non-recursive term should not have recursion" $
    not (hasRecursion term)

testDirectRecursion :: Assertion
testDirectRecursion = do
  -- A term that calls itself: f x
  let term = App (Var (var "f")) (Var (var "x"))
  assertBool "Application of f should be detected as having recursion" $
    hasRecursion term

testRecursionInApp :: Assertion
testRecursionInApp = do
  -- f (g x)
  let term = App (Var (var "f")) (App (Var (var "g")) (Var (var "x")))
  assertBool "Recursion in nested application should be detected" $
    hasRecursion term

testRecursionInCase :: Assertion
testRecursionInCase = do
  -- case x of P -> f y
  let scrutinee = Var (var "x")
      c1 = clause (patVar "y") (App (Var (var "f")) (Var (var "y")))
      term = Case scrutinee [c1] Nothing
  assertBool "Recursion in case body should be detected" $
    hasRecursion term

-- ============================================================================
-- SIMPLE RECURSION PATTERN TESTS
-- ============================================================================

testStructuralRecursion :: Assertion
testStructuralRecursion = do
  -- length (Cons h t) = 1 + length t
  -- Simplified: just the recursive call part
  let funcName = "length"
      -- App of length to a term
      term = App (Var (var "length")) (Var (var "t"))

  case checkTermination emptyEnv funcName term of
    Right () -> return ()  -- Should succeed
    Left err -> assertFailure $ "Structural recursion should pass: " ++ prettyTermError err

testNonTerminating :: Assertion
testNonTerminating = do
  -- f x = f (x + 1)
  let funcName = "f"
      -- The recursive call f (x + 1)
      successor = App (Const (PrimOp OpAdd)) (Var (var "x"))
      term = App (Var (var "f")) successor

  case checkTermination emptyEnv funcName term of
    Left (NonTerminatingRecursion _) -> return ()  -- Should fail
    Left err -> assertFailure $ "Wrong error type: " ++ show err
    Right () -> assertFailure "Non-terminating recursion should be rejected"

testGuardedRecursion :: Assertion
testGuardedRecursion = do
  -- Simplified guarded recursion
  let funcName = "countdown"
      -- countdown n = if n <= 0 then ... else countdown (n - 1)
      -- Just the recursive part
      term = App (Var (var "countdown")) (Var (var "n"))

  case checkTermination emptyEnv funcName term of
    Right () -> return ()  -- Should succeed (structural pattern)
    Left err -> assertFailure $ "Guarded recursion should pass: " ++ prettyTermError err

-- ============================================================================
-- RECURSIVE CALL ANALYSIS TESTS
-- ============================================================================

testFindDirectCall :: Assertion
testFindDirectCall = do
  let funcName = "length"
      term = App (Var (var "length")) (Var (var "t"))

  case analyzeRecursiveCalls funcName term of
    Right calls ->
      case calls of
        [RecursiveCall name _ _] ->
          assertEqual "Should find call to length" name "length"
        [] -> assertFailure "Should find at least one recursive call"
        _ -> assertFailure "Should find exactly one recursive call"
    Left err -> assertFailure $ "Analysis should succeed: " ++ prettyTermError err

testStructuralCallPattern :: Assertion
testStructuralCallPattern = do
  let funcName = "walk"
      -- walk (tree)
      term = App (Var (var "walk")) (Constr (qn "Leaf") [Var (var "x")])

  case analyzeRecursiveCalls funcName term of
    Right [RecursiveCall _ pattern _] ->
      assertEqual "Should detect structural call pattern" pattern (StructuralCall "constr")
    _ -> assertFailure "Should find structural call pattern"

testUnsupportedPattern :: Assertion
testUnsupportedPattern = do
  let funcName = "strange"
      -- A recursive call with unknown pattern
      unhandledOp = App (Const (PrimOp OpDiv)) (Var (var "x"))
      term = App (Var (var "strange")) unhandledOp

  case analyzeRecursiveCalls funcName term of
    Right calls ->
      case calls of
        [RecursiveCall _ UnknownPattern _] -> return ()
        _ -> assertFailure "Should detect unknown pattern"
    Left err -> assertFailure $ "Analysis should still work: " ++ prettyTermError err

-- ============================================================================
-- MUTUAL RECURSION TESTS
-- ============================================================================

testMutualRecursionProgress :: Assertion
testMutualRecursionProgress = do
  -- even n = if n == 0 then true else odd (n - 1)
  -- odd n = if n == 0 then false else even (n - 1)
  let functions = [("even", Var (var "x")), ("odd", Var (var "y"))]

  case checkTerminationMulti emptyEnv functions of
    Right () -> return ()  -- Should succeed
    Left err -> assertFailure $ "Mutual recursion with progress should pass: " ++ prettyTermError err

testInfiniteMutualRecursion :: Assertion
testInfiniteMutualRecursion = do
  -- f x = g x
  -- g x = f x
  -- No progress on any argument
  let functions = [("f", Var (var "x")), ("g", Var (var "x"))]

  case checkTerminationMulti emptyEnv functions of
    Left (InfiniteCallCycle _) -> return ()  -- Should detect cycle
    Left err -> assertFailure $ "Wrong error type: " ++ show err
    Right () -> assertFailure "Infinite cycle should be rejected"

testCallCycleDetection :: Assertion
testCallCycleDetection = do
  -- a calls b, b calls c, c calls a
  let functions = [("a", Var (var "b")), ("b", Var (var "c")), ("c", Var (var "a"))]

  case checkTerminationMulti emptyEnv functions of
    Left (InfiniteCallCycle cycle_path) ->
      assertBool "Cycle should be detected" (length cycle_path > 0)
    _ -> assertFailure "Should detect cycle in call graph"

-- ============================================================================
-- INTEGRATION TESTS
-- ============================================================================

testLengthFunction :: Assertion
testLengthFunction = do
  -- Simulate length :: List a -> Nat
  -- length Nil = 0
  -- length (Cons _ t) = 1 + length t
  let funcName = "length"
      -- The recursive case: length t
      consCase = Constr (qn "Cons") [Var (var "h"), Var (var "t")]
      recursiveCall = App (Var (var "length")) (Var (var "t"))

  case checkTermination emptyEnv funcName recursiveCall of
    Right () -> return ()
    Left err -> assertFailure $ "length should terminate: " ++ prettyTermError err

testFibonacciFunction :: Assertion
testFibonacciFunction = do
  -- fib n = if n <= 1 then n else fib(n-1) + fib(n-2)
  -- For simplicity, just check the recursive call part
  let funcName = "fib"
      -- fib (n - 1)
      term = App (Var (var "fib")) (Var (var "n"))

  case checkTermination emptyEnv funcName term of
    Right () -> return ()
    Left err -> assertFailure $ "fib should pass initial check: " ++ prettyTermError err

testMapFunction :: Assertion
testMapFunction = do
  -- map :: (a -> b) -> List a -> List b
  -- map f [] = []
  -- map f (x:xs) = f x : map f xs
  let funcName = "map"
      -- map f xs
      recursiveCall = App (Var (var "map")) (Var (var "xs"))

  case checkTermination emptyEnv funcName recursiveCall of
    Right () -> return ()
    Left err -> assertFailure $ "map should terminate: " ++ prettyTermError err

-- ============================================================================
-- ERROR MESSAGE TESTS
-- ============================================================================

testNonTerminatingError :: Assertion
testNonTerminatingError = do
  let err = NonTerminatingRecursion "recursive call does not decrease"
  let msg = prettyTermError err
  assertBool "Error message should mention non-termination" $
    "does not terminate" `elem` words msg

testUnsupportedPatternError :: Assertion
testUnsupportedPatternError = do
  let err = UnsupportedRecursionPattern "unknown pattern found"
  let msg = prettyTermError err
  assertBool "Error message should mention unsupported pattern" $
    "pattern" `elem` words msg

testCycleDetectionError :: Assertion
testCycleDetectionError = do
  let cycle_path = ["f", "g", "f"]
  let err = InfiniteCallCycle cycle_path
  let msg = prettyTermError err
  assertBool "Error message should mention cycle" $
    "cycle" `elem` words msg