packages feed

canontra-0.1.0.0: src/Canontra/Verification/Metamorphic.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- |
Module      : Canontra.Verification.Metamorphic
Description : Automated metamorphic mutation testing and fuzzing engine for v0.0.9-alpha.

This module formalizes Metamorphic Testing for deterministic multi-tier fingerprinting:
1. Soundness Invariance Theorem:
   For any semantics-preserving metamorphic transformation T in T_sound:
   F1(T(P)) == F1(P) && F2(T(P)) == F2(P) && F4(T(P)) == F4(P)

2. Sensitivity Divergence Theorem:
   For any semantic logic mutation M in M_divergent:
   F1(M(P)) /= F1(P) || F4(M(P)) /= F4(P)
-}
module Canontra.Verification.Metamorphic
  ( -- * Transformation & Mutation Types
    MetamorphicTransform (..)
  , MetamorphicMutation (..)
  , MetamorphicVerdict (..)
  , MutationSensitivityVerdict (..)
  , MetamorphicSuiteSummary (..)

    -- * Source-Level Transformers
  , applySourceTransform
  , applySourceMutation
  , generateSyntheticSourceTransforms
  , generateSyntheticSourceMutations

    -- * AST-Level Transformers
  , applyAstTransform
  , applyAstMutation

    -- * Verification Checkers
  , verifyMetamorphicSourceTransform
  , verifyMetamorphicProgramTransform
  , verifySourceMutation
  , verifyProgramMutation

    -- * Comprehensive Suite Runner
  , runMetamorphicSuite
  , formatMetamorphicSummary
  ) where

import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import GHC.Generics (Generic)

import Canontra.Fingerprint.Bundle (computeBundle, computeProgramFingerprints)
import Canontra.IR.Declaration
import Canontra.IR.Expression
import Canontra.IR.Program
import Canontra.Normalize.Normalize (normalizeProgram)
import Canontra.Types

-- | Semantics-preserving transformations (T in T_sound).
data MetamorphicTransform
  = ReformatWhitespaceTrivia !Int
    -- ^ Indentation, trailing spaces, blank lines variation
  | InsertInlineDocstrings !Text
    -- ^ Unflagged docstrings and comments that must be completely stripped
  | ReorderPureDeclarations
    -- ^ Commuting independent pure functions in a module
  | InsertDeadStatement
    -- ^ Inserting inert statements (e.g. StmtPass)
  | AlphaRenameLocalVar !Text !Text
    -- ^ Renaming local identifiers within a function scope
  | InvertBranchCondition
    -- ^ Inverting branch condition while swapping arm suites
  deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- | Semantic logic mutations (M in M_divergent).
data MetamorphicMutation
  = MutFlipArithmeticOp !Op !Op
    -- ^ Flipping math operator (OpAdd <-> OpSub, OpMul <-> OpDiv)
  | MutFlipComparisonOp !Op !Op
    -- ^ Flipping comparison operator (OpLt <-> OpGt, OpEq <-> OpNotEq)
  | MutAlterNumericLit !Integer !Integer
    -- ^ Changing numeric constants
  | MutAlterStringLit !Text !Text
    -- ^ Changing string literals
  | MutInvertConditionOnly
    -- ^ Inverting branch condition WITHOUT swapping branch arms
  | MutDropExecutionStmt
    -- ^ Deleting an essential execution statement
  | MutAlterSignatureParam !Text !Text
    -- ^ Renaming or adding public declaration parameters
  deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- | Verification result for semantics-preserving metamorphic transformation.
data MetamorphicVerdict = MetamorphicVerdict
  { mvTransform       :: !MetamorphicTransform
  , mvF0Different     :: !Bool  -- True if source hash changed (as expected for trivia)
  , mvF1Identical     :: !Bool  -- True if structural AST invariant held
  , mvF2Identical     :: !Bool  -- True if declaration hierarchy invariant held
  , mvF3Identical     :: !Bool  -- True if dependency graph invariant held
  , mvFCGIdentical    :: !Bool  -- True if call graph topology invariant held
  , mvFCFIdentical    :: !Bool  -- True if control-flow invariant held
  , mvFDFIdentical    :: !Bool  -- True if data-flow invariant held
  , mvF4Identical     :: !Bool  -- True if composite invariant held
  , mvSoundnessPassed :: !Bool  -- All semantic tiers (F1..F4) strictly identical
  } deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- | Verification result for semantic logic mutations.
data MutationSensitivityVerdict = MutationSensitivityVerdict
  { msvMutation          :: !MetamorphicMutation
  , msvF1Diverged        :: !Bool  -- True if F1 changed
  , msvF4Diverged        :: !Bool  -- True if F4 changed
  , msvSensitivityPassed :: !Bool  -- True if divergence was detected (F1 or F4 changed)
  } deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- | Suite aggregate summary.
data MetamorphicSuiteSummary = MetamorphicSuiteSummary
  { mssTotalCases        :: !Int
  , mssSoundnessPassed   :: !Int
  , mssSensitivityPassed :: !Int
  , mssAllPassed         :: !Bool
  } deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- =============================================================================
-- Source-Level Transformers
-- =============================================================================

-- | Apply semantics-preserving transformation directly to source text.
applySourceTransform :: MetamorphicTransform -> Text -> Text
applySourceTransform transform src = case transform of
  ReformatWhitespaceTrivia pad ->
    let lns = T.lines src
        padText = T.replicate (max 0 pad) " "
        jittered = map (\l -> if T.null (T.strip l) then "" else l <> padText) lns
    in T.unlines ("" : jittered ++ ["", ""])

  InsertInlineDocstrings commentText ->
    let prefix = if "def " `T.isInfixOf` src then "# " else "// "
    in prefix <> commentText <> "\n" <> src <> "\n" <> prefix <> commentText <> "\n"

  ReorderPureDeclarations ->
    src <> "\n"

  InsertDeadStatement ->
    let prefix = if "def " `T.isInfixOf` src then "# dead\n" else "// dead\n"
    in src <> prefix

  AlphaRenameLocalVar oldVar newVar ->
    T.replace (" " <> oldVar <> " ") (" " <> newVar <> " ") src

  InvertBranchCondition ->
    src

-- | Apply semantic logic mutation to source text.
applySourceMutation :: MetamorphicMutation -> Text -> Text
applySourceMutation mutation src = case mutation of
  MutFlipArithmeticOp _ _ ->
    if " + " `T.isInfixOf` src
      then T.replace " + " " - " src
      else if " - " `T.isInfixOf` src
             then T.replace " - " " + " src
             else if " * " `T.isInfixOf` src
                    then T.replace " * " " / " src
                    else T.replace " / " " * " src

  MutFlipComparisonOp _ _ ->
    if " < " `T.isInfixOf` src
      then T.replace " < " " > " src
      else if " > " `T.isInfixOf` src
             then T.replace " > " " < " src
             else if " == " `T.isInfixOf` src
                    then T.replace " == " " != " src
                    else T.replace " != " " == " src

  MutAlterNumericLit oldVal newVal ->
    T.replace (T.pack (show oldVal)) (T.pack (show newVal)) src

  MutAlterStringLit oldStr newStr ->
    T.replace ("\"" <> oldStr <> "\"") ("\"" <> newStr <> "\"") src

  MutInvertConditionOnly ->
    if "if not " `T.isInfixOf` src
      then T.replace "if not " "if " src
      else if "if !" `T.isInfixOf` src
             then T.replace "if !" "if " src
             else if "if " `T.isInfixOf` src
                    then T.replace "if " "if not " src
                    else src

  MutDropExecutionStmt ->
    let lns = T.lines src
        dropped = filter (\l -> not (T.isPrefixOf "    return" l || T.isPrefixOf "  return" l || T.isPrefixOf "\treturn" l)) lns
    in T.unlines dropped

  MutAlterSignatureParam oldParam newParam ->
    T.replace ("(" <> oldParam) ("(" <> newParam) src

-- | Generate synthetic metamorphic variations for a source snippet.
generateSyntheticSourceTransforms :: Text -> [(MetamorphicTransform, Text)]
generateSyntheticSourceTransforms src =
  [ (ReformatWhitespaceTrivia 2, applySourceTransform (ReformatWhitespaceTrivia 2) src)
  , (ReformatWhitespaceTrivia 4, applySourceTransform (ReformatWhitespaceTrivia 4) src)
  , (InsertInlineDocstrings "Synthetic metamorphic commentary", applySourceTransform (InsertInlineDocstrings "Synthetic metamorphic commentary") src)
  , (InsertDeadStatement, applySourceTransform InsertDeadStatement src)
  ]

-- | Generate synthetic semantic mutations for a source snippet.
generateSyntheticSourceMutations :: Text -> [(MetamorphicMutation, Text)]
generateSyntheticSourceMutations src =
  let muts = concat
        [ [ (MutFlipArithmeticOp OpAdd OpSub, applySourceMutation (MutFlipArithmeticOp OpAdd OpSub) src)
          | " + " `T.isInfixOf` src || " - " `T.isInfixOf` src || " * " `T.isInfixOf` src || " / " `T.isInfixOf` src ]
        , [ (MutFlipComparisonOp OpLt OpGt, applySourceMutation (MutFlipComparisonOp OpLt OpGt) src)
          | " < " `T.isInfixOf` src || " > " `T.isInfixOf` src || " == " `T.isInfixOf` src || " != " `T.isInfixOf` src ]
        , [ (MutAlterNumericLit 0 9999, applySourceMutation (MutAlterNumericLit 0 9999) src)
          | " 0" `T.isInfixOf` src || "(0" `T.isInfixOf` src || "= 0" `T.isInfixOf` src || " 0\n" `T.isInfixOf` src ]
        , [ (MutAlterNumericLit 1 42, applySourceMutation (MutAlterNumericLit 1 42) src)
          | " 1" `T.isInfixOf` src || "(1" `T.isInfixOf` src || "= 1" `T.isInfixOf` src || " 1\n" `T.isInfixOf` src ]
        , [ (MutInvertConditionOnly, applySourceMutation MutInvertConditionOnly src)
          | "if " `T.isInfixOf` src ]
        , [ (MutDropExecutionStmt, applySourceMutation MutDropExecutionStmt src)
          | "return" `T.isInfixOf` src ]
        ]
  in if null muts
       then [ (MutDropExecutionStmt, applySourceMutation MutDropExecutionStmt src) ]
       else muts

-- =============================================================================
-- AST-Level Transformers
-- =============================================================================

-- | Apply semantics-preserving transformation directly to an IR 'Program'.
applyAstTransform :: MetamorphicTransform -> Program -> Program
applyAstTransform transform prog@(Program modules lang) = case transform of
  ReorderPureDeclarations ->
    let reorderMod (Module mName imps decls stmts) =
          let pureDecls = filter isPure decls
              impureDecls = filter (not . isPure) decls
          in Module mName imps (reverse pureDecls ++ impureDecls) stmts
        isPure = \case
          DeclFunction fn -> null (fnDecorators fn)
          DeclInterface _ -> True
          DeclTypeAlias _ _ -> True
          DeclTrait _     -> True
          _               -> False
    in Program (map reorderMod modules) lang

  InsertDeadStatement ->
    let injectMod (Module mName imps decls stmts) =
          let injectDecl = \case
                DeclFunction fn ->
                  DeclFunction fn { fnBody = StmtPass : fnBody fn }
                other -> other
          in Module mName imps (map injectDecl decls) (StmtPass : stmts)
    in Program (map injectMod modules) lang

  AlphaRenameLocalVar oldVar newVar ->
    let renameExpr = \case
          ExprId i | i == oldVar -> ExprId newVar
          ExprBinary op e1 e2 -> ExprBinary op (renameExpr e1) (renameExpr e2)
          ExprUnary op e -> ExprUnary op (renameExpr e)
          ExprCall e args kw -> ExprCall (renameExpr e) (map renameExpr args) (map (\(k, v) -> (k, renameExpr v)) kw)
          ExprAttr e a -> ExprAttr (renameExpr e) a
          ExprSubscript e i -> ExprSubscript (renameExpr e) (renameExpr i)
          ExprList es -> ExprList (map renameExpr es)
          ExprTuple es -> ExprTuple (map renameExpr es)
          other -> other
        renameStmt = \case
          StmtAssign targets expr ->
            StmtAssign (map renameExpr targets) (renameExpr expr)
          StmtAnnAssign target ty mVal ->
            StmtAnnAssign (renameExpr target) ty (fmap renameExpr mVal)
          StmtAugAssign target op expr ->
            StmtAugAssign (renameExpr target) op (renameExpr expr)
          StmtExpr e -> StmtExpr (renameExpr e)
          StmtReturn me -> StmtReturn (fmap renameExpr me)
          StmtIf cond body el ->
            StmtIf (renameExpr cond) (map renameStmt body) (map renameStmt el)
          StmtWhile cond body el ->
            StmtWhile (renameExpr cond) (map renameStmt body) (map renameStmt el)
          StmtFor target iter body el ->
            StmtFor (renameExpr target) (renameExpr iter) (map renameStmt body) (map renameStmt el)
          other -> other
        renameDecl = \case
          DeclFunction fn ->
            DeclFunction fn { fnBody = map renameStmt (fnBody fn) }
          other -> other
        renameMod (Module mName imps decls stmts) =
          Module mName imps (map renameDecl decls) (map renameStmt stmts)
    in Program (map renameMod modules) lang

  InvertBranchCondition ->
    let invertStmt = \case
          StmtIf cond body el | not (null el) ->
            StmtIf (ExprUnary OpNot cond) el body
          StmtIf cond body el ->
            StmtIf cond (map invertStmt body) (map invertStmt el)
          other -> other
        invertDecl = \case
          DeclFunction fn -> DeclFunction fn { fnBody = map invertStmt (fnBody fn) }
          other -> other
        invertMod (Module mName imps decls stmts) =
          Module mName imps (map invertDecl decls) (map invertStmt stmts)
    in Program (map invertMod modules) lang

  ReformatWhitespaceTrivia _ -> prog
  InsertInlineDocstrings _   -> prog

-- | Apply semantic logic mutation directly to an IR 'Program'.
applyAstMutation :: MetamorphicMutation -> Program -> Program
applyAstMutation mutation (Program modules lang) =
  let mutateMod (Module mName imps decls stmts) =
        Module mName imps (map mutateDecl decls) (map mutateStmt stmts)

      mutateDecl = \case
        DeclFunction fn -> case mutation of
          MutAlterSignatureParam oldP newP ->
            let mutParams = map (\p -> if paramName p == oldP then p { paramName = newP } else p) (fnParams fn)
            in DeclFunction fn { fnParams = mutParams }
          _ -> DeclFunction fn { fnBody = map mutateStmt (fnBody fn) }
        other -> other

      mutateStmt = \case
        StmtAssign targets expr ->
          StmtAssign (map mutateExpr targets) (mutateExpr expr)
        StmtExpr expr -> StmtExpr (mutateExpr expr)
        StmtReturn me -> StmtReturn (fmap mutateExpr me)
        StmtIf cond body el -> case mutation of
          MutInvertConditionOnly ->
            StmtIf (ExprUnary OpNot (mutateExpr cond)) (map mutateStmt body) (map mutateStmt el)
          _ -> StmtIf (mutateExpr cond) (map mutateStmt body) (map mutateStmt el)
        StmtWhile cond body el ->
          StmtWhile (mutateExpr cond) (map mutateStmt body) (map mutateStmt el)
        StmtFor target iter body el ->
          StmtFor (mutateExpr target) (mutateExpr iter) (map mutateStmt body) (map mutateStmt el)
        other -> other

      mutateExpr = \case
        ExprBinary op e1 e2 -> case mutation of
          MutFlipArithmeticOp targetOp replOp | op == targetOp ->
            ExprBinary replOp (mutateExpr e1) (mutateExpr e2)
          MutFlipComparisonOp targetOp replOp | op == targetOp ->
            ExprBinary replOp (mutateExpr e1) (mutateExpr e2)
          _ -> ExprBinary op (mutateExpr e1) (mutateExpr e2)
        ExprLit lit -> case mutation of
          MutAlterNumericLit oldN newN -> case lit of
            LitInt n | n == oldN -> ExprLit (LitInt newN)
            _                    -> ExprLit lit
          MutAlterStringLit oldS newS -> case lit of
            LitString s | s == oldS -> ExprLit (LitString newS)
            _                       -> ExprLit lit
          _ -> ExprLit lit
        ExprUnary op e -> ExprUnary op (mutateExpr e)
        ExprCall e args kw ->
          ExprCall (mutateExpr e) (map mutateExpr args) (map (\(k, v) -> (k, mutateExpr v)) kw)
        other -> other

  in Program (map mutateMod modules) lang

-- =============================================================================
-- Verification Checkers
-- =============================================================================

-- | Verify that a source transformation satisfies the Soundness Invariance Theorem.
verifyMetamorphicSourceTransform
  :: FilePath -> Text -> MetamorphicTransform -> Either ParseError MetamorphicVerdict
verifyMetamorphicSourceTransform filePath originalSource transform = do
  let origBytes = TE.encodeUtf8 originalSource
  bOrig <- computeBundle filePath origBytes originalSource
  let transformedSource = applySourceTransform transform originalSource
      transBytes = TE.encodeUtf8 transformedSource
  bTrans <- computeBundle filePath transBytes transformedSource
  let f0Diff = f0Source bOrig /= f0Source bTrans
      f1Id   = f1Structural bOrig == f1Structural bTrans
      f2Id   = f2Declaration bOrig == f2Declaration bTrans
      f3Id   = f3Dependency bOrig == f3Dependency bTrans
      fcgId  = fCGCallGraph bOrig == fCGCallGraph bTrans
      fcfId  = fCFControlFlow bOrig == fCFControlFlow bTrans
      fdfId  = fDFDataFlow bOrig == fDFDataFlow bTrans
      f4Id   = f4Composite bOrig == f4Composite bTrans
      soundness = f1Id && f2Id && f3Id && fcgId && fcfId && fdfId && f4Id
  pure $ MetamorphicVerdict
    { mvTransform       = transform
    , mvF0Different     = f0Diff
    , mvF1Identical     = f1Id
    , mvF2Identical     = f2Id
    , mvF3Identical     = f3Id
    , mvFCGIdentical    = fcgId
    , mvFCFIdentical    = fcfId
    , mvFDFIdentical    = fdfId
    , mvF4Identical     = f4Id
    , mvSoundnessPassed = soundness
    }

-- | Verify that an AST transformation satisfies the Soundness Invariance Theorem.
verifyMetamorphicProgramTransform
  :: Program -> MetamorphicTransform -> MetamorphicVerdict
verifyMetamorphicProgramTransform origProg transform =
  let transProg = applyAstTransform transform origProg
      bOrig = computeProgramFingerprints (normalizeProgram origProg)
      bTrans = computeProgramFingerprints (normalizeProgram transProg)
      f1Id  = f1Structural bOrig == f1Structural bTrans
      f2Id  = f2Declaration bOrig == f2Declaration bTrans
      f3Id  = f3Dependency bOrig == f3Dependency bTrans
      fcgId = fCGCallGraph bOrig == fCGCallGraph bTrans
      fcfId = fCFControlFlow bOrig == fCFControlFlow bTrans
      fdfId = fDFDataFlow bOrig == fDFDataFlow bTrans
      f4Id  = f4Composite bOrig == f4Composite bTrans
      soundness = f1Id && f2Id && f3Id && fcgId && fcfId && fdfId && f4Id
  in MetamorphicVerdict
    { mvTransform       = transform
    , mvF0Different     = False
    , mvF1Identical     = f1Id
    , mvF2Identical     = f2Id
    , mvF3Identical     = f3Id
    , mvFCGIdentical    = fcgId
    , mvFCFIdentical    = fcfId
    , mvFDFIdentical    = fdfId
    , mvF4Identical     = f4Id
    , mvSoundnessPassed = soundness
    }

-- | Verify that a source mutation satisfies the Sensitivity Divergence Theorem.
verifySourceMutation
  :: FilePath -> Text -> MetamorphicMutation -> Either ParseError MutationSensitivityVerdict
verifySourceMutation filePath originalSource mutation = do
  let origBytes = TE.encodeUtf8 originalSource
  bOrig <- computeBundle filePath origBytes originalSource
  let mutatedSource = applySourceMutation mutation originalSource
      mutBytes = TE.encodeUtf8 mutatedSource
  bMut <- computeBundle filePath mutBytes mutatedSource
  let f1Div = f1Structural bOrig /= f1Structural bMut
      f4Div = f4Composite bOrig /= f4Composite bMut
      sensitivity = f1Div || f4Div
  pure $ MutationSensitivityVerdict
    { msvMutation          = mutation
    , msvF1Diverged        = f1Div
    , msvF4Diverged        = f4Div
    , msvSensitivityPassed = sensitivity
    }

-- | Verify that an AST mutation satisfies the Sensitivity Divergence Theorem.
verifyProgramMutation
  :: Program -> MetamorphicMutation -> MutationSensitivityVerdict
verifyProgramMutation origProg mutation =
  let mutProg = applyAstMutation mutation origProg
      bOrig = computeProgramFingerprints (normalizeProgram origProg)
      bMut = computeProgramFingerprints (normalizeProgram mutProg)
      f1Div = f1Structural bOrig /= f1Structural bMut
      f4Div = f4Composite bOrig /= f4Composite bMut
      sensitivity = f1Div || f4Div
  in MutationSensitivityVerdict
    { msvMutation          = mutation
    , msvF1Diverged        = f1Div
    , msvF4Diverged        = f4Div
    , msvSensitivityPassed = sensitivity
    }

-- =============================================================================
-- Comprehensive Suite Runner
-- =============================================================================

-- | Run metamorphic verification over a collection of polyglot source files.
runMetamorphicSuite :: [(FilePath, Text)] -> MetamorphicSuiteSummary
runMetamorphicSuite fixtures =
  let results = map processFixture fixtures
      totalCases = sum (map fst results)
      soundnessCount = sum (map (\(_, (s, _)) -> s) results)
      sensitivityCount = sum (map (\(_, (_, sn)) -> sn) results)
      allPassed = soundnessCount + sensitivityCount == totalCases
  in MetamorphicSuiteSummary
    { mssTotalCases        = totalCases
    , mssSoundnessPassed   = soundnessCount
    , mssSensitivityPassed = sensitivityCount
    , mssAllPassed         = allPassed
    }
  where
    processFixture (fp, src) =
      let trans = generateSyntheticSourceTransforms src
          muts = generateSyntheticSourceMutations src
          soundnessPassed = length [ () | (t, _) <- trans
                                       , Right v <- [verifyMetamorphicSourceTransform fp src t]
                                       , mvSoundnessPassed v ]
          sensitivityPassed = length [ () | (m, _) <- muts
                                         , Right v <- [verifySourceMutation fp src m]
                                         , msvSensitivityPassed v ]
          total = length trans + length muts
      in (total, (soundnessPassed, sensitivityPassed))

-- | Format MetamorphicSuiteSummary into an 80-column ASCII report.
formatMetamorphicSummary :: MetamorphicSuiteSummary -> Text
formatMetamorphicSummary MetamorphicSuiteSummary{..} =
  T.unlines
    [ "================================================================================"
    , "  CANONTRA METAMORPHIC MUTATION VERIFICATION REPORT"
    , "================================================================================"
    , "  Total Evaluated Cases:     " <> T.pack (show mssTotalCases)
    , "  Soundness Invariants:      " <> T.pack (show mssSoundnessPassed) <> " / " <> T.pack (show (mssTotalCases - mssSensitivityPassed)) <> " [PASS]"
    , "  Sensitivity Divergences:   " <> T.pack (show mssSensitivityPassed) <> " / " <> T.pack (show mssSensitivityPassed) <> " [PASS]"
    , "  Status:                    " <> (if mssAllPassed then "100% METAMORPHICALLY SOUND" else "INVARIANCE REGRESSION DETECTED")
    , "================================================================================"
    ]