packages feed

qute-0.1.0: test/Analysis.hs

-- SPDX-FileCopyrightText: 2025 Sören Tempel <soeren+git@soeren-tempel.net>
-- SPDX-FileCopyrightText: 2026 Reliable System Software, Technische Universität Braunschweig <vss@ibr.cs.tu-bs.de>
--
-- SPDX-License-Identifier: GPL-3.0-only

module Analysis (analTests) where

import Data.Bifunctor (bimap)
import Data.List (sort)
import Language.QBE (parseAndFind)
import Language.QBE.Analysis.CDG qualified as CDG
import Language.QBE.Analysis.CFG qualified as CFG
import Language.QBE.Types qualified as QBE
import System.FilePath ((</>))
import Test.Tasty
import Test.Tasty.HUnit

getFunction :: QBE.GlobalIdent -> String -> IO QBE.FuncDef
getFunction funcName input = snd <$> parseAndFind funcName input

getFuncAndProg :: FilePath -> QBE.GlobalIdent -> IO QBE.FuncDef
getFuncAndProg fileName funcName =
  let filePath = "test" </> "testdata" </> fileName
   in readFile filePath >>= getFunction funcName

toBlkName :: CFG.CFG -> CFG.Label -> String
toBlkName cfg = show . CFG.labelToIdent cfg

cdgEdges :: CFG.CFG -> CDG.CDG -> [(String, String)]
cdgEdges cfg = map go . CDG.edges
  where
    go (f, t) = (toBlkName cfg f, toBlkName cfg t)

cfgEdges :: CFG.CFG -> [(String, String)]
cfgEdges cfg =
  let toBlk = toBlkName cfg
   in sort $ map (bimap toBlk toBlk) (CFG.edges cfg)

------------------------------------------------------------------------

analTests :: TestTree
analTests =
  testGroup
    "Analysis tests"
    [ testCase "Simple CFG without any loops" $
        do
          func <-
            getFunction
              (QBE.GlobalIdent "foo")
              "function w $foo() {\n\
              \@start\n\
              \%val =w add 0, 1\n\
              \jmp @next\n\
              \@next\n\
              \ret\n\
              \}\n"

          let cfg = CFG.build func
          let startLabel = CFG.identToLabel cfg $ QBE.BlockIdent "start"
          map (CFG.labelToIdent cfg) (CFG.lookupSuccs cfg startLabel)
            @?= [QBE.BlockIdent "next"]

          cfgEdges cfg
            @?= [("@start", "@next")]

          -- “If Y is control dependent on X then X must have two exits.“, in
          -- this CFG there are no nodes with two exits: The CDG must be emtpy.
          let ret = CFG.identToLabel cfg $ QBE.BlockIdent "next"
              cdg = CDG.build cfg ret
          cdgEdges cfg cdg @?= []
          CDG.ctrlDeps cdg ret @?= Nothing,
      testCase "Generate CDG for code with single branch" $
        do
          func <-
            getFunction
              (QBE.GlobalIdent "foo")
              "function w $foo() {\n\
              \@start\n\
              \%val =w add 0, 1\n\
              \jnz %val, @ifT, @ifF\n\
              \@ifT\n\
              \%ret =w copy 1\n\
              \jmp @return\n\
              \@ifF\n\
              \%ret =w copy 0\n\
              \jmp @return\n\
              \@return\n\
              \ret %ret\n\
              \}\n"

          let cfg = CFG.build func
              ret = CFG.identToLabel cfg (QBE.BlockIdent "return")
              cdg = CDG.build cfg ret

          cdgEdges cfg cdg
            @?= [ ("@ifF", "@start"),
                  ("@ifT", "@start")
                ],
      testCase "Compute CDG for code with loop" $
        do
          func <-
            getFunction
              (QBE.GlobalIdent "main")
              "function w $main() {\n\
              \@start\n\
              \%.1 =w copy 0\n\
              \%.2 =w copy 42\n\
              \%.3 =w copy 0\n\
              \@for_cond\n\
              \%.6 =w csltw %.3, %.2\n\
              \jnz %.6, @for_body, @for_join\n\
              \@for_body\n\
              \%.1 =w add %.1, 1\n\
              \@for_cont\n\
              \%.3 =w add %.3, 1\n\
              \jmp @for_cond\n\
              \@for_join\n\
              \ret %.11\n\
              \}\n"

          let cfg = CFG.build func
              ret = CFG.identToLabel cfg (QBE.BlockIdent "for_join")
              cdg = CDG.build cfg ret

          cdgEdges cfg cdg
            @?= [ ("@for_body", "@for_cond"),
                  ("@for_cond", "@for_cond"),
                  ("@for_cont", "@for_cond")
                ],
      testCase "Compute CDG for code with two paths to node" $
        do
          func <- getFuncAndProg "disjunction.qbe" (QBE.GlobalIdent "main")

          let cfg = CFG.build func
              ret = CFG.identToLabel cfg (QBE.BlockIdent "return")
              cdg = CDG.build cfg ret

          cdgEdges cfg cdg
            @?= [ ("@if_false.4", "@body.2"),
                  ("@if_false.4", "@if_true.3"),
                  ("@if_false.6", "@if_true.3"),
                  ("@if_join.7", "@if_true.3"),
                  ("@if_true.3", "@body.2"),
                  ("@if_true.5", "@if_true.3")
                ],
      testCase "Compute dominators for a simple-cc representation" $
        do
          func <- getFuncAndProg "simple-cc-branches.qbe" (QBE.GlobalIdent "myfunc")

          let cfg = CFG.build func
          CFG.labelToIdent cfg (CFG.startNode cfg)
            @?= QBE.BlockIdent ".L9"
    ]