packages feed

canontra-0.1.0.0: test/Canontra/NormalizeSpec.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : Canontra.NormalizeSpec
Description : Unit test specification for Normalizer v4 (Phase 4).

Tests pure declaration canonical sorting, stateful declaration order preservation,
runtime reflection docstring preservation (:preserve:, @preserve, :doc:),
decorator-level docstring retention (@preserve_docstring, @reflect, @doc),
and mathematical idempotence N(N(P)) == N(P).
-}
module Canontra.NormalizeSpec (spec) where

import Test.Hspec

import Canontra.Fingerprint.Declaration (computeF2)
import Canontra.Fingerprint.Structural (computeF1)
import Canontra.IR.Declaration
import Canontra.IR.Expression
import Canontra.IR.Program
import Canontra.Normalize.Normalize
import Canontra.Parser.Python (parsePythonSource)
import Canontra.Types (unFingerprint)

spec :: Spec
spec = do
  describe "Normalizer v4 - Declaration Classification & Ordering" $ do
    it "classifies undecorated functions and interfaces as provably pure" $ do
      let pureFn = DeclFunction (Function "pure_calc" [] Nothing [] [] False)
          decFn  = DeclFunction (Function "cached_calc" [] Nothing ["@lru_cache"] [] False)
          iface  = DeclInterface (Interface "Reader" [] [])
          alias  = DeclTypeAlias "UserID" Nothing
          trait  = DeclTrait (Trait "Display" [] [])
          cls    = DeclClass (Class "Service" [] [] [])
          st     = DeclStruct (Struct "Config" [] [] "pub")
      isProvablyPureDeclaration pureFn `shouldBe` True
      isProvablyPureDeclaration iface `shouldBe` True
      isProvablyPureDeclaration alias `shouldBe` True
      isProvablyPureDeclaration trait `shouldBe` True
      isProvablyPureDeclaration decFn `shouldBe` False
      isProvablyPureDeclaration cls `shouldBe` False
      isProvablyPureDeclaration st `shouldBe` False

    it "canonically sorts pure functions by declIdentifier" $ do
      let fnZ = DeclFunction (Function "zeta" [] Nothing [] [] False)
          fnA = DeclFunction (Function "alpha" [] Nothing [] [] False)
          fnM = DeclFunction (Function "mu" [] Nothing [] [] False)
          sorted = normalizeModuleDeclarations [fnZ, fnA, fnM]
          names = [fnName fn | DeclFunction fn <- sorted]
      names `shouldBe` ["alpha", "mu", "zeta"]

    it "preserves source execution order for stateful declarations (classes, decorated functions)" $ do
      let clsZ = DeclClass (Class "ZetaClass" [] [] [])
          clsA = DeclClass (Class "AlphaClass" [] [] [])
          decZ = DeclFunction (Function "zeta_dec" [] Nothing ["@dec"] [] False)
          decA = DeclFunction (Function "alpha_dec" [] Nothing ["@dec"] [] False)
          normalized = normalizeModuleDeclarations [clsZ, clsA, decZ, decA]
      map declIdentifier normalized `shouldBe` ["cls:ZetaClass", "cls:AlphaClass", "fn:zeta_dec", "fn:alpha_dec"]

    it "partitions pure declarations first (sorted) followed by stateful declarations in source order" $ do
      let fnZ = DeclFunction (Function "zeta_pure" [] Nothing [] [] False)
          clsB = DeclClass (Class "BetaClass" [] [] [])
          fnA = DeclFunction (Function "alpha_pure" [] Nothing [] [] False)
          clsA = DeclClass (Class "AlphaClass" [] [] [])
          normalized = normalizeModuleDeclarations [fnZ, clsB, fnA, clsA]
      map declIdentifier normalized `shouldBe` ["fn:alpha_pure", "fn:zeta_pure", "cls:BetaClass", "cls:AlphaClass"]

    it "guarantees commutativity: reordering pure functions yields identical F1 and F2 fingerprints" $ do
      let code1 = "def gamma(): pass\ndef alpha(): pass\ndef beta(): pass\n"
          code2 = "def alpha(): pass\ndef beta(): pass\ndef gamma(): pass\n"
      case (parsePythonSource "1.py" code1, parsePythonSource "2.py" code2) of
        (Right p1, Right p2) -> do
          let f1_1 = computeF1 p1
              f1_2 = computeF1 p2
              f2_1 = computeF2 p1
              f2_2 = computeF2 p2
          unFingerprint f1_1 `shouldBe` unFingerprint f1_2
          unFingerprint f2_1 `shouldBe` unFingerprint f2_2
        _ -> expectationFailure "Parse failed"

  describe "Normalizer v4 - Runtime Reflection Docstring Preservation" $ do
    it "preserves docstrings marked with :preserve: in AST" $ do
      let code = "def compute():\n    \"\"\":preserve: Critical reflection metadata\"\"\"\n    return 42\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclFunction fn] _] -> do
              let body = fnBody fn
              length body `shouldBe` 2
              case head body of
                StmtExpr (ExprLit (LitString s)) -> s `shouldBe` ":preserve: Critical reflection metadata"
                _ -> expectationFailure "Expected leading preserved docstring"
            _ -> expectationFailure "Expected single module with single function declaration"

    it "preserves docstrings marked with @preserve" $ do
      let code = "def validate():\n    \"\"\"@preserve runtime validator contract\"\"\"\n    return True\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclFunction fn] _] -> length (fnBody fn) `shouldBe` 2
            _ -> expectationFailure "Expected single module with single function declaration"

    it "preserves docstrings marked with :doc:" $ do
      let code = "def api_endpoint():\n    \"\"\":doc: OpenAPI specification summary\"\"\"\n    return 200\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclFunction fn] _] -> length (fnBody fn) `shouldBe` 2
            _ -> expectationFailure "Expected single module with single function declaration"

    it "strips unflagged standard docstrings" $ do
      let code = "def standard():\n    \"\"\"Unflagged docstring to strip.\"\"\"\n    return 100\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclFunction fn] _] -> do
              let body = fnBody fn
              length body `shouldBe` 1
              case head body of
                StmtReturn _ -> pure ()
                _ -> expectationFailure "Expected docstring to be stripped"
            _ -> expectationFailure "Expected single module with single function declaration"

    it "preserves docstrings in functions decorated with @preserve_docstring" $ do
      let code = "@preserve_docstring\ndef handler():\n    \"\"\"Standard text preserved via decorator\"\"\"\n    return 1\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclFunction fn] _] -> length (fnBody fn) `shouldBe` 2
            _ -> expectationFailure "Expected single module with single function declaration"

    it "preserves docstrings in classes decorated with @preserve_docstring across methods" $ do
      let code = "@preserve_docstring\nclass Model:\n    def predict(self):\n        \"\"\"Inference docstring\"\"\"\n        return 0\n"
      case parsePythonSource "test.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let normProg = normalizeProgram prog
          case progModules normProg of
            [Module _ _ [DeclClass cls] _] ->
              case clsMethods cls of
                [m] -> length (fnBody m) `shouldBe` 2
                _ -> expectationFailure "Expected single method"
            _ -> expectationFailure "Expected single class declaration"

    it "differentiates structural hash F1 when reflection docstrings differ" $ do
      let code1 = "def test():\n    \"\"\":preserve: Spec version 1.0\"\"\"\n    return 1\n"
          code2 = "def test():\n    \"\"\":preserve: Spec version 2.0\"\"\"\n    return 1\n"
      case (parsePythonSource "1.py" code1, parsePythonSource "2.py" code2) of
        (Right p1, Right p2) -> do
          computeF1 p1 `shouldNotBe` computeF1 p2
        _ -> expectationFailure "Parse failed"

  describe "Normalizer v4 - Algebraic Idempotence & Pass Parity" $ do
    it "guarantees Normalizer v4 idempotence N(N(P)) == N(P)" $ do
      let code = "class Svc:\n    def a(self): pass\ndef z(): pass\ndef b(): pass\n"
      case parsePythonSource "idemp.py" code of
        Left err -> expectationFailure (show err)
        Right prog -> do
          let n1 = normalizeProgram prog
              n2 = normalizeProgram n1
          n1 `shouldBe` n2

    it "guarantees declaration normalization idempotence N(N(D)) == N(D)" $ do
      let fnZ = DeclFunction (Function "z" [] Nothing [] [] False)
          fnA = DeclFunction (Function "a" [] Nothing [] [] False)
          cls = DeclClass (Class "C" [] [] [])
          decls = [fnZ, cls, fnA]
          norm1 = normalizeModuleDeclarations decls
          norm2 = normalizeModuleDeclarations norm1
      norm1 `shouldBe` norm2