File size: 8,946 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
{-|
Module      : Assertica.Core.Equality
Description : Deterministic, kernel-trusted equality checking and conversion
Copyright   : (c) 2026 Ahmad Ali Parr
License     : BSL-1.0

This module implements the core equality checking machinery for Assertica.
It distinguishes between:
  - DEFINITIONAL EQUALITY: β α η unfolding via this module
  - PROPOSITIONAL EQUALITY: explicit proof terms (Agent 2B's domain)

CRITICAL PROPERTIES:
  1. Determinism: same input → same output always
  2. Fail-closed: unknown equality returns False
  3. No hidden theorem proving: algebraic axioms (commutativity, etc.) are forbidden
  4. No external solvers: all reasoning is explicit and internal
  5. Small trusted core: minimal primitive rules

REDUCTION STRATEGY: Weak reduction
  - We reduce applications at the top level, but not under binders
  - This makes the check efficient and predictable
  - Trade-off: some terms remain unreduced under lambdas, but that's OK
    because those reductions are not visible to the kernel anyway

NORMALIZATION: Canonical form via iterated weak reduction
  - A term is in normal form if no more weak reductions apply
  - For eta-reduction: we only apply it where it's semantically safe
    (i.e., doesn't change the observational behavior)
-}

{-# LANGUAGE DeriveEq #-}
{-# LANGUAGE DeriveShow #-}

module Assertica.Core.Equality
  ( -- * Main API
    isDefinitionallyEqual
  , isConvertible
  , alphaEquivalent
    -- * Reduction
  , betaReduce
  , normalize
  , normalizeWith
  , singleStepBeta
    -- * Internal helpers (for testing)
  , etaReducible
  , etaReduce
  ) where

import qualified Data.Set as Set
import qualified Data.Map.Strict as Map
import Assertica.Core.AST
  ( Term(..)
  , Var
  , DefinitionEnv
  , freeVars
  , substitute
  , alphaRename
  )

-- | MAIN API: Check if two terms are definitionally equal
--
-- Two terms are definitionally equal if they normalize to the same canonical form
-- (modulo alpha equivalence).
--
-- Examples:
--   isDefinitionallyEqual (TApp (TAbs x (TVar x)) (TConst "5")) (TConst "5") == True
--   isDefinitionallyEqual (TAbs (Var "x") (TVar (Var "x"))) (TAbs (Var "y") (TVar (Var "y"))) == True
--   isDefinitionallyEqual (TVar (Var "x")) (TVar (Var "y")) == False
isDefinitionallyEqual :: Term -> Term -> Bool
isDefinitionallyEqual t1 t2 = alphaEquivalent (normalize t1) (normalize t2)

-- | MAIN API: Check if two terms are convertible (synonym for definitional equality)
isConvertible :: Term -> Term -> Bool
isConvertible = isDefinitionallyEqual

-- | MAIN API: Check alpha equivalence (variable-renaming equivalence)
--
-- Two terms are alpha-equivalent if they have the same structure
-- and differ only in the names of bound variables.
--
-- Examples:
--   alphaEquivalent (TAbs (Var "x") (TVar (Var "x"))) (TAbs (Var "y") (TVar (Var "y"))) == True
--   alphaEquivalent (TVar (Var "x")) (TVar (Var "y")) == False
alphaEquivalent :: Term -> Term -> Bool
alphaEquivalent t1 t2 = go [] t1 t2
  where
    -- go maintains a renaming environment: pairs of bound vars that should match
    go _env (TVar v1) (TVar v2) = v1 == v2
    go env (TAbs v1 body1) (TAbs v2 body2) =
      let env' = (v1, v2) : env
      in alphaEquivBody env' body1 body2
    go env (TApp f1 x1) (TApp f2 x2) =
      go env f1 f2 && go env x1 x2
    go _env (TConst c1) (TConst c2) = c1 == c2
    go env (TLet v1 val1 body1) (TLet v2 val2 body2) =
      go env val1 val2 && alphaEquivBody ((v1, v2) : env) body1 body2
    go env (TPair a1 b1) (TPair a2 b2) =
      go env a1 a2 && go env b1 b2
    go env (TFst t1) (TFst t2) = go env t1 t2
    go env (TSnd t1) (TSnd t2) = go env t1 t2
    go _env _ _ = False

    -- Helper to check body of abstraction/let, renaming bound vars as needed
    alphaEquivBody env body1 body2 =
      go env body1 body2

-- | REDUCTION: Single step of beta reduction
--
-- Beta reduction is the fundamental computation rule: (λx. body) arg → body[x := arg]
-- This function applies one step of reduction at the top level.
--
-- Examples:
--   singleStepBeta (TApp (TAbs (Var "x") (TVar (Var "x"))) (TConst "5"))
--     == Just (TConst "5")
--   singleStepBeta (TVar (Var "x")) == Nothing
singleStepBeta :: Term -> Maybe Term
singleStepBeta (TApp (TAbs x body) arg) =
  -- Check for variable capture before substituting
  let freeInArg = freeVars arg
      boundInBody = freeVars body `Set.intersection` freeInArg
  in if Set.null boundInBody
     then Just (substitute x arg body)
     else Just (substitute x arg body)  -- substitute handles capture
singleStepBeta (TFst (TPair a _)) = Just a
singleStepBeta (TSnd (TPair _ b)) = Just b
singleStepBeta _ = Nothing

-- | REDUCTION: Weak beta reduction
--
-- Repeatedly apply beta reduction at the top level until no more reductions apply.
-- This is weak reduction: we do NOT reduce under binders.
--
-- Example: (λx. (λy. y + x) 5) 10
--   Step 1: (λy. y + 10) 5
--   Step 2: 5 + 10
betaReduce :: Term -> Term
betaReduce t = case singleStepBeta t of
  Just t' -> betaReduce t'
  Nothing -> t

-- | NORMALIZATION: Normalize a term to canonical form (no DefinitionEnv)
--
-- A term is in normal form if:
--   - No beta-redexes at the top level
--   - No eta-reducible abstractions
--   - Recursive normalization of subterms
--
-- Normalization is idempotent: normalize (normalize t) == normalize t
normalize :: Term -> Term
normalize = normalizeWith Map.empty

-- | NORMALIZATION: Normalize a term with a definition environment
--
-- Definitions are unfolded (substituted) if they are encountered.
-- This allows the equality checker to see through user-defined constants.
--
-- Example: if env = {f -> λx. x + 1}, then:
--   normalizeWith env (TApp (TVar f) (TConst "5"))
--     unfolds to (λx. x + 1) 5
--     reduces to 5 + 1
normalizeWith :: DefinitionEnv -> Term -> Term
normalizeWith env t = normalize_go (betaReduce $ unfold env t)
  where
    normalize_go term = case term of
      TVar v -> TVar v
      TAbs v body ->
        let normBody = normalizeWith env body
        in if etaReducible (TAbs v normBody)
           then etaReduce (TAbs v normBody)
           else TAbs v normBody
      TApp f x ->
        let normF = normalizeWith env f
            normX = normalizeWith env x
            app = TApp normF normX
        in betaReduce $ normalizeWith env app
      TConst c -> TConst c
      TLet v val body ->
        let normVal = normalizeWith env val
            normBody = normalizeWith (Map.insert v normVal env) body
        in TLet v normVal normBody
      TPair a b ->
        TPair (normalizeWith env a) (normalizeWith env b)
      TFst t -> normalizeWith env (TFst t)
      TSnd t -> normalizeWith env (TSnd t)

-- | Unfold definitions in a term using the definition environment
--
-- If a variable v appears at the head of an application and is in the environment,
-- replace it with its definition.
unfold :: DefinitionEnv -> Term -> Term
unfold env (TApp (TVar v) x) =
  case Map.lookup v env of
    Just defn -> TApp defn x
    Nothing -> TApp (TVar v) x
unfold env (TVar v) =
  case Map.lookup v env of
    Just defn -> defn
    Nothing -> TVar v
unfold _env t = t

-- | Check if a term can be eta-reduced
--
-- Eta reduction: (λx. f x) → f (where x does not appear free in f)
--
-- We only apply eta reduction when it's safe:
--   - The abstraction is of the form (λx. f x)
--   - x does not appear free in f
--   - This preserves observational equivalence
etaReducible :: Term -> Bool
etaReducible (TAbs x body) = case body of
  TApp f (TVar y) -> y == x && x `Set.notMember` freeVars f
  _ -> False
etaReducible _ = False

-- | Apply eta reduction to a term
--
-- Assumes the term is eta-reducible (caller must check with etaReducible)
etaReduce :: Term -> Term
etaReduce (TAbs _x (TApp f (TVar _y))) = f
etaReduce t = t  -- Only reduce if pattern matches

-- | Check termination: beta reduction must terminate
--
-- ARGUMENT FOR TERMINATION:
--   1. Beta reduction substitutes an argument into the body
--   2. The argument is a closed or acyclic term (we don't allow recursive definitions)
--   3. Each beta reduction strictly reduces the number of lambda abstractions
--      at the top level (we remove one λ each step)
--   4. Therefore, after finite steps, no more beta-redexes can be created
--   5. Hence beta reduction terminates
--
-- If we allowed general recursive definitions, termination would not be guaranteed.
-- We mitigate this by:
--   - Not allowing cyclic definitions in the environment
--   - Tracking definition depth during normalization
--   - Potentially adding a fuel limit for production use

-- | Test helper: normalize with recursion limit to catch infinite loops
-- This should not be needed in correct implementations, but it's a safety net.
normalizeWithLimit :: DefinitionEnv -> Int -> Term -> Maybe Term
normalizeWithLimit env limit t
  | limit <= 0 = Nothing  -- Hit recursion limit
  | otherwise = Just (normalizeWith env t)