packages feed

sv2v-0.0.13: src/Bugpoint.hs

{-# LANGUAGE ScopedTypeVariables #-}
{- sv2v
 - Author: Zachary Snow <zach@zachjs.com>
 -
 - Utility for reducing test cases that cause conversion errors or otherwise
 - produce unexpected output.
 -}

module Bugpoint (runBugpoint) where

import System.Exit (exitFailure)
import System.IO (hPutStrLn, stderr)

import Control.Exception (catches, ErrorCall(..), Handler(..), PatternMatchFail(..))
import Control.Monad (when, (>=>))
import Data.Functor ((<&>))
import Data.List (isInfixOf)

import qualified Convert.RemoveComments
import Language.SystemVerilog.AST

runBugpoint :: [String] -> ([AST] -> IO [AST]) -> [AST] -> IO [AST]
runBugpoint expected converter =
    fmap pure . runBugpoint' expected converter' . concat
    where
        converter' :: AST -> IO AST
        converter' = fmap concat . converter . pure

runBugpoint' :: [String] -> (AST -> IO AST) -> AST -> IO AST
runBugpoint' expected converter ast = do
    ast' <- runBugpointPass expected converter ast
    if ast == ast'
        then out "done minimizing" >> return ast
        else runBugpoint' expected converter ast'

out :: String -> IO ()
out = hPutStrLn stderr . ("bugpoint: " ++)

-- run the given converter and return the conversion failure if any or the
-- converted output otherwise
extractConversionResult :: (AST -> IO AST) -> AST -> IO String
extractConversionResult converter asts =
    catches runner
        [ Handler handleErrorCall
        , Handler handlePatternMatchFail
        ]
    where
        runner = converter asts <&> show
        handleErrorCall (ErrorCall str) = return str
        handlePatternMatchFail (PatternMatchFail str) = return str

runBugpointPass :: [String] -> (AST -> IO AST) -> AST -> IO AST
runBugpointPass expected converter ast = do
    out $ "beginning pass with " ++ (show $ length $ show ast) ++ " characters"
    matches <- oracle ast
    when (not matches) $
        out ("doesn't match expected strings: " ++ show expected) >> exitFailure
    let ast' = concat $ Convert.RemoveComments.convert [ast]
    matches' <- oracle ast'
    minimizeContainer oracle minimizeDescription "<design>" id $
        if matches' then ast' else ast
    where
        oracle :: AST -> IO Bool
        oracle = fmap check . extractConversionResult converter
        check :: String -> Bool
        check = flip all expected . flip isInfixOf

type Oracle t = t -> IO Bool
type Minimizer t = Oracle t -> t -> IO t

-- given a subsequence-verifying oracle, a strategy for minimizing within
-- elements of the sequence, a name for debugging, a constructor for the
-- container, and the elements within the container, produce a minimized version
-- of the container
minimizeContainer :: forall a b. (Show a, Show b)
    => Oracle a -> Minimizer b -> String -> ([b] -> a) -> [b] -> IO a
minimizeContainer oracle minimizer name constructor =
    stepFilter 0 [] >=>
    stepRecurse [] >=>
    return . constructor
    where
        oracle' :: Oracle [b]
        oracle' = oracle . constructor

        stepFilter :: Int -> [b] -> [b] -> IO [b]
        stepFilter 0 [] pending = stepFilter (length pending) [] pending
        stepFilter 1 need [] = return need
        stepFilter width need [] =
            stepFilter (max 1 $ width `div` 4) [] need
        stepFilter width need pending = do
            matches <- oracle' $ need ++ rest
            if matches
                then out msg >> stepFilter width need rest
                else stepFilter width (need ++ curr) rest
            where
                (curr, rest) = splitAt width pending
                msg = "removed " ++ show (length curr) ++ " items from " ++ name

        stepRecurse :: [b] -> [b] -> IO [b]
        stepRecurse before [] = return before
        stepRecurse before (isolated : after) = do
            isolated' <- minimizer oracleRecurse isolated
            stepRecurse (before ++ [isolated']) after
            where oracleRecurse = (oracle' $) . (before ++) . (: after)

minimizeDescription :: Minimizer Description
minimizeDescription oracle (Package lifetime name items) =
    minimizeContainer oracle (const return) name constructor items
    where constructor = Package lifetime name
minimizeDescription oracle (Part att ext kw lif name ports items) =
    minimizeContainer oracle minimizeModuleItem name constructor items
    where constructor = Part att ext kw lif name ports
minimizeDescription _ other = return other

minimizeModuleItem :: Minimizer ModuleItem
minimizeModuleItem oracle (Generate items) =
    minimizeContainer oracle minimizeGenItem "<generate>" Generate items
minimizeModuleItem _ item = return item

minimizeGenItem :: Minimizer GenItem
minimizeGenItem _ GenNull = return GenNull
minimizeGenItem oracle item = do
    matches <- oracle GenNull
    if matches
        then out "removed generate item" >> return GenNull
        else minimizeGenItem' oracle item

minimizeGenItem' :: Minimizer GenItem
minimizeGenItem' oracle (GenModuleItem item) =
    minimizeModuleItem (oracle . GenModuleItem) item <&> GenModuleItem
minimizeGenItem' oracle (GenIf c t f) = do
    t' <- minimizeGenItem (oracle . flip (GenIf c) f) t
    f' <- minimizeGenItem (oracle . GenIf c t') f
    return $ GenIf c t' f'
minimizeGenItem' _ (GenBlock _ []) = return GenNull
minimizeGenItem' oracle (GenBlock name items) =
    minimizeContainer oracle minimizeGenItem' name constructor items
    where constructor = GenBlock name
minimizeGenItem' oracle (GenFor a b c item) =
    minimizeGenItem (oracle . constructor) item <&> constructor
    where constructor = GenFor a b c
minimizeGenItem' oracle (GenCase expr cases) =
    minimizeContainer oracle minimizeGenCase "<case>" constructor cases
    where constructor = GenCase expr
minimizeGenItem' _ GenNull = return GenNull

minimizeGenCase :: Minimizer GenCase
minimizeGenCase oracle (exprs, item) =
    minimizeGenItem' (oracle . constructor) item <&> constructor
    where constructor = (exprs,)