packages feed

canontra-0.1.0.0: src/Canontra/Analysis/CFG.hs

{- |
Module      : Canontra.Analysis.CFG
Description : Control-Flow Graph (CFG) builder and basic block partitioner.

This module partitions function bodies into basic blocks, tracks conditional
branch edges, loop back-edges, switches, match cases, and exception jumps
in linear time O(|V| + |E|).
-}
module Canontra.Analysis.CFG
  ( BlockId
  , BranchCondition (..)
  , BlockTerminator (..)
  , BasicBlock (..)
  , CFGEdge (..)
  , ControlFlowGraph (..)
  , buildCFGs
  , buildFunctionCFG
  , formatCFG
  ) where

import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, ToJSON)
import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)

import Canontra.IR.Declaration
import Canontra.IR.Expression
import Canontra.IR.Program

type BlockId = Int

data BranchCondition
  = CondTrue Expr
  | CondFalse Expr
  | CondCase Expr
  | CondDefault
  | CondUnconditional
  | CondException Text
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

data BlockTerminator
  = TermReturn (Maybe Expr)
  | TermBranch Expr BlockId BlockId
  | TermJump BlockId
  | TermSwitch Expr [(Expr, BlockId)] (Maybe BlockId)
  | TermRaise (Maybe Expr)
  | TermExit
  deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

data BasicBlock = BasicBlock
  { bbId         :: BlockId
  , bbStatements :: [Stmt]
  , bbTerminator :: BlockTerminator
  } deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

data CFGEdge = CFGEdge
  { edgeFrom      :: BlockId
  , edgeTo        :: BlockId
  , edgeCondition :: BranchCondition
  } deriving stock (Eq, Ord, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

data ControlFlowGraph = ControlFlowGraph
  { cfgFunction :: Text
  , cfgEntry    :: BlockId
  , cfgBlocks   :: [BasicBlock]
  , cfgEdges    :: [CFGEdge]
  } deriving stock (Eq, Show, Generic)
  deriving anyclass (ToJSON, FromJSON, NFData)

-- | Build CFGs for all functions, methods, and receivers in a Program.
buildCFGs :: Program -> [ControlFlowGraph]
buildCFGs (Program modules _) =
  concatMap extractModuleCFGs modules

extractModuleCFGs :: Module -> [ControlFlowGraph]
extractModuleCFGs (Module modName _ decls stmts) =
  let topCFG = if null stmts then [] else [buildFunctionCFG ("<top-level:" <> modName <> ">") stmts]
      declCFGs = concatMap extractDeclCFGs decls
  in topCFG ++ declCFGs

extractDeclCFGs :: Declaration -> [ControlFlowGraph]
extractDeclCFGs decl = case decl of
  DeclFunction fn ->
    [buildFunctionCFG (fnName fn) (fnBody fn)]
  DeclClass cls ->
    [buildFunctionCFG (clsName cls <> "." <> fnName m) (fnBody m) | m <- clsMethods cls]
  DeclStruct st ->
    [buildFunctionCFG (stName st <> "." <> fnName m) (fnBody m) | m <- stMethods st]
  DeclTrait tr ->
    [buildFunctionCFG (trName tr <> "." <> fnName m) (fnBody m) | m <- trMethods tr]
  DeclImpl imp ->
    [buildFunctionCFG (impTarget imp <> "." <> fnName m) (fnBody m) | m <- impMethods imp]
  DeclReceiver rc fn ->
    [buildFunctionCFG (rcTypeName rc <> "." <> fnName fn) (fnBody fn)]
  _ -> []

-- | Build a ControlFlowGraph for a sequence of statements with entry block 0.
buildFunctionCFG :: Text -> [Stmt] -> ControlFlowGraph
buildFunctionCFG fnName stmts =
  let (blocks, edges, _) = partitionBlocks 0 stmts 1
      sortedBlocks = sortBy (comparing bbId) blocks
      sortedEdges  = sortBy (comparing (\e -> (edgeFrom e, edgeTo e, edgeCondition e))) edges
  in ControlFlowGraph fnName 0 sortedBlocks sortedEdges

partitionBlocks :: BlockId -> [Stmt] -> BlockId -> ([BasicBlock], [CFGEdge], BlockId)
partitionBlocks curId [] nextId =
  ([BasicBlock curId [] TermExit], [], nextId)

partitionBlocks curId (s:ss) nextId = case s of
  StmtReturn me ->
    let block = BasicBlock curId [StmtReturn me] (TermReturn me)
    in ([block], [], nextId)

  StmtRaise me _ ->
    let block = BasicBlock curId [StmtRaise me Nothing] (TermRaise me)
    in ([block], [], nextId)

  StmtIf cond thenStmts elseStmts ->
    let thenBlockId = nextId
        (thenBlocks, thenEdges, nextId1) = partitionBlocks thenBlockId thenStmts (thenBlockId + 1)
        elseBlockId = nextId1
        (elseBlocks, elseEdges, nextId2) = partitionBlocks elseBlockId elseStmts (elseBlockId + 1)
        joinBlockId = nextId2
        (joinBlocks, joinEdges, nextId3) = partitionBlocks joinBlockId ss (joinBlockId + 1)

        (condBlocks, condEdges, nextId4) = decomposeCondition curId cond thenBlockId elseBlockId nextId3
        joinFromThen = [CFGEdge thenBlockId joinBlockId CondUnconditional | not (null ss)]
        joinFromElse = [CFGEdge elseBlockId joinBlockId CondUnconditional | not (null ss)]

        allBlocks = condBlocks ++ thenBlocks ++ elseBlocks ++ joinBlocks
        allEdges  = condEdges ++ joinFromThen ++ joinFromElse ++ thenEdges ++ elseEdges ++ joinEdges
    in (allBlocks, allEdges, nextId4)

  StmtWhile cond bodyStmts elseStmts ->
    let bodyBlockId = nextId
        (bodyBlocks, bodyEdges, nextId1) = partitionBlocks bodyBlockId bodyStmts (bodyBlockId + 1)
        exitBlockId = nextId1
        (exitBlocks, exitEdges, nextId2) = partitionBlocks exitBlockId (elseStmts ++ ss) (exitBlockId + 1)

        (condBlocks, condEdges, nextId3) = decomposeCondition curId cond bodyBlockId exitBlockId nextId2
        edges = condEdges ++ [CFGEdge bodyBlockId curId CondUnconditional] ++ bodyEdges ++ exitEdges
        blocks = condBlocks ++ bodyBlocks ++ exitBlocks
    in (blocks, edges, nextId3)

  StmtFor _ iter bodyStmts elseStmts ->
    let bodyBlockId = nextId
        (bodyBlocks, bodyEdges, nextId1) = partitionBlocks bodyBlockId bodyStmts (bodyBlockId + 1)
        exitBlockId = nextId1
        (exitBlocks, exitEdges, nextId2) = partitionBlocks exitBlockId (elseStmts ++ ss) (exitBlockId + 1)

        headerBlock = BasicBlock curId [] (TermBranch iter bodyBlockId exitBlockId)
        edges =
          [ CFGEdge curId bodyBlockId (CondTrue iter)
          , CFGEdge curId exitBlockId (CondFalse iter)
          , CFGEdge bodyBlockId curId CondUnconditional
          ] ++ bodyEdges ++ exitEdges
        blocks = headerBlock : (bodyBlocks ++ exitBlocks)
    in (blocks, edges, nextId2)

  StmtLoop bodyStmts ->
    let bodyBlockId = nextId
        (bodyBlocks, bodyEdges, nextId1) = partitionBlocks bodyBlockId bodyStmts (bodyBlockId + 1)
        headerBlock = BasicBlock curId [] (TermJump bodyBlockId)
        edges = [CFGEdge curId bodyBlockId CondUnconditional, CFGEdge bodyBlockId curId CondUnconditional] ++ bodyEdges
    in (headerBlock : bodyBlocks, edges, nextId1)

  StmtSwitch expr cases defaultStmts ->
    let (caseBlocks, caseEdges, caseTerms, nextId1) = foldl stepCase ([], [], [], nextId) cases
        exitBlockId = nextId1
        (defBlocks, defEdges, nextId2) = partitionBlocks exitBlockId (defaultStmts ++ ss) (exitBlockId + 1)
        headerBlock = BasicBlock curId [] (TermSwitch expr caseTerms (Just exitBlockId))
        caseHeaderEdges = [CFGEdge curId bId (CondCase cExpr) | (cExpr, bId) <- caseTerms]
        defHeaderEdge = CFGEdge curId exitBlockId CondDefault
        allBlocks = headerBlock : (caseBlocks ++ defBlocks)
        allEdges = caseHeaderEdges ++ [defHeaderEdge] ++ caseEdges ++ defEdges
    in (allBlocks, allEdges, nextId2)
    where
      stepCase (bAcc, eAcc, tAcc, nId) (cExpr, cStmts) =
        let cBlockId = nId
            (cBlocks, cEdges, nId1) = partitionBlocks cBlockId cStmts (cBlockId + 1)
        in (bAcc ++ cBlocks, eAcc ++ cEdges, tAcc ++ [(cExpr, cBlockId)], nId1)

  StmtMatch expr cases ->
    let (caseBlocks, caseEdges, caseTerms, nextId1) = foldl stepCase ([], [], [], nextId) cases
        exitBlockId = nextId1
        (exitBlocks, exitEdges, nextId2) = partitionBlocks exitBlockId ss (exitBlockId + 1)
        headerBlock = BasicBlock curId [] (TermSwitch expr caseTerms (Just exitBlockId))
        caseHeaderEdges = [CFGEdge curId bId (CondCase cExpr) | (cExpr, bId) <- caseTerms]
        allBlocks = headerBlock : (caseBlocks ++ exitBlocks)
        allEdges = caseHeaderEdges ++ caseEdges ++ exitEdges
    in (allBlocks, allEdges, nextId2)
    where
      stepCase (bAcc, eAcc, tAcc, nId) mc =
        let cBlockId = nId
            (cBlocks, cEdges, nId1) = partitionBlocks cBlockId (mcBody mc) (cBlockId + 1)
        in (bAcc ++ cBlocks, eAcc ++ cEdges, tAcc ++ [(mcPattern mc, cBlockId)], nId1)

  StmtTry tryBody handlers elseBody finallyBody ->
    let joinBlockId = nextId
        (joinBlocks, joinEdges, nextId1) = partitionBlocks joinBlockId ss (joinBlockId + 1)

        (hasFinally, finBlockId, finBlocks, finEdges, nextId2) =
          if null finallyBody
          then (False, joinBlockId, [], [], nextId1)
          else
            let fId = nextId1
                (fBlocks, fEdges, n2) = partitionBlocks fId finallyBody (fId + 1)
                fJoinEdge = [CFGEdge fId joinBlockId CondUnconditional | not (null ss)]
            in (True, fId, fBlocks, fEdges ++ fJoinEdge, n2)

        afterTryTarget = finBlockId

        (elseBlockId, elseBlocks, elseEdges, nextId3) =
          if null elseBody
          then (afterTryTarget, [], [], nextId2)
          else
            let eId = nextId2
                (eBlocks, eEdges, n3) = partitionBlocks eId elseBody (eId + 1)
                eTargetEdge = [CFGEdge eId afterTryTarget CondUnconditional]
            in (eId, eBlocks, eEdges ++ eTargetEdge, n3)

        (caseBlocks, caseEdges, handlerEntries, nextId4) =
          foldl (stepHandler afterTryTarget) ([], [], [], nextId3) handlers

        tryBlockId = nextId4
        (tryBlocks, tryEdges, nextId5) = partitionBlocks tryBlockId tryBody (tryBlockId + 1)

        entryBlock = BasicBlock curId [] (TermJump tryBlockId)
        entryEdge = CFGEdge curId tryBlockId CondUnconditional

        tryToElseEdge = [CFGEdge tryBlockId elseBlockId CondUnconditional]

        handlerEdges =
          [ CFGEdge tryBlockId hId (CondException (formatExcExpr mExpr))
          | (mExpr, hId) <- handlerEntries
          ]

        unwindEdge =
          [ CFGEdge tryBlockId finBlockId (CondException "*")
          | hasFinally
          ]

        allBlocks = entryBlock : (tryBlocks ++ elseBlocks ++ caseBlocks ++ finBlocks ++ joinBlocks)
        allEdges =
          [entryEdge]
          ++ tryToElseEdge
          ++ handlerEdges
          ++ unwindEdge
          ++ tryEdges
          ++ elseEdges
          ++ caseEdges
          ++ finEdges
          ++ joinEdges
    in (allBlocks, allEdges, nextId5)
    where
      stepHandler targetId (bAcc, eAcc, hAcc, nId) (mExcExpr, _, hStmts) =
        let hBlockId = nId
            (hBlocks, hEdges, nId1) = partitionBlocks hBlockId hStmts (hBlockId + 1)
            hExitEdge = [CFGEdge hBlockId targetId CondUnconditional]
        in (bAcc ++ hBlocks, eAcc ++ hEdges ++ hExitEdge, hAcc ++ [(mExcExpr, hBlockId)], nId1)

  _ ->
    -- Collect non-branching statements into current block
    let (linear, rest) = span isLinearStmt (s:ss)
    in case rest of
      [] ->
        ([BasicBlock curId linear TermExit], [], nextId)
      (r:rs) ->
        let nextBlockId = nextId
            (nextBlocks, nextEdges, nextId1) = partitionBlocks nextBlockId (r:rs) (nextBlockId + 1)
            thisBlock = BasicBlock curId linear (TermJump nextBlockId)
            edge = CFGEdge curId nextBlockId CondUnconditional
        in (thisBlock : nextBlocks, edge : nextEdges, nextId1)

decomposeCondition :: BlockId -> Expr -> BlockId -> BlockId -> BlockId -> ([BasicBlock], [CFGEdge], BlockId)
decomposeCondition curBId (ExprBinary OpAnd left right) trueTarget falseTarget nextAvailId =
  let rightBlockId = nextAvailId
      (leftBlocks, leftEdges, nextId1) = decomposeCondition curBId left rightBlockId falseTarget (rightBlockId + 1)
      (rightBlocks, rightEdges, nextId2) = decomposeCondition rightBlockId right trueTarget falseTarget nextId1
  in (leftBlocks ++ rightBlocks, leftEdges ++ rightEdges, nextId2)
decomposeCondition curBId (ExprBinary OpOr left right) trueTarget falseTarget nextAvailId =
  let rightBlockId = nextAvailId
      (leftBlocks, leftEdges, nextId1) = decomposeCondition curBId left trueTarget rightBlockId (rightBlockId + 1)
      (rightBlocks, rightEdges, nextId2) = decomposeCondition rightBlockId right trueTarget falseTarget nextId1
  in (leftBlocks ++ rightBlocks, leftEdges ++ rightEdges, nextId2)
decomposeCondition curBId expr trueTarget falseTarget nextAvailId =
  let thisBlock = BasicBlock curBId [] (TermBranch expr trueTarget falseTarget)
      branchEdges =
        [ CFGEdge curBId trueTarget (CondTrue expr)
        , CFGEdge curBId falseTarget (CondFalse expr)
        ]
  in ([thisBlock], branchEdges, nextAvailId)

formatExcExpr :: Maybe Expr -> Text
formatExcExpr Nothing = "*"
formatExcExpr (Just (ExprId name)) = name
formatExcExpr (Just (ExprAttr _ name)) = name
formatExcExpr (Just _) = "*"

isLinearStmt :: Stmt -> Bool
isLinearStmt = \case
  StmtIf {}       -> False
  StmtWhile {}    -> False
  StmtFor {}      -> False
  StmtAsyncFor {} -> False
  StmtLoop {}     -> False
  StmtReturn {}   -> False
  StmtRaise {}    -> False
  StmtMatch {}    -> False
  StmtSwitch {}   -> False
  StmtTry {}      -> False
  _               -> True

formatCFG :: ControlFlowGraph -> Text
formatCFG cfg =
  T.unlines $
    [ "CFG: " <> cfgFunction cfg <> " (Blocks: " <> T.pack (show (length (cfgBlocks cfg))) <> ", Edges: " <> T.pack (show (length (cfgEdges cfg))) <> ")"
    , "---------------------------------------------------------"
    ] ++
    map formatBlock (cfgBlocks cfg) ++
    [ "Edges:" ] ++
    map formatEdge (cfgEdges cfg)
  where
    formatBlock b =
      "  [Block " <> T.pack (show (bbId b)) <> "] (" <> T.pack (show (length (bbStatements b))) <> " stmts) -> " <> formatTerm (bbTerminator b)

    formatTerm = \case
      TermReturn _       -> "Return"
      TermBranch _ t f   -> "Branch -> True: " <> T.pack (show t) <> ", False: " <> T.pack (show f)
      TermJump j         -> "Jump -> " <> T.pack (show j)
      TermSwitch _ _ _   -> "Switch"
      TermRaise _        -> "Raise"
      TermExit           -> "Exit"

    formatEdge e =
      "    " <> T.pack (show (edgeFrom e)) <> " ---> " <> T.pack (show (edgeTo e)) <> " [" <> formatCond (edgeCondition e) <> "]"

    formatCond = \case
      CondTrue _        -> "true"
      CondFalse _       -> "false"
      CondCase _        -> "case"
      CondDefault       -> "default"
      CondUnconditional -> "uncond"
      CondException ex  -> "except: " <> ex