packages feed

crucible-debug-0.1.0: src/Lang/Crucible/Debug/Style.hs

{-|
Copyright        : (c) Galois, Inc. 2025
Maintainer       : Langston Barrett <langston@galois.com>
-}

{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StandaloneDeriving #-}

module Lang.Crucible.Debug.Style
  ( StyleEnv(..)
  , StyleT(..)
  , runStyle
  , runStyleM
  , Style(..)
  , Valid(..)
  , Styled(..)
  , style
  , highlighter
  ) where

import Control.Lens qualified as Lens
import Control.Monad.IO.Class (MonadIO)
import Control.Monad.Reader (MonadReader, ReaderT)
import Control.Monad.Reader qualified as Reader
import Control.Monad.Trans (MonadTrans)
import Data.Foldable (foldl', toList)
import Data.Functor.Identity (Identity, runIdentity)
import Data.List qualified as List
import Data.Parameterized.Some (Some(Some), viewSome)
import Data.Sequence qualified as Seq
import Data.Set qualified as Set
import Data.Set (Set)
import Data.Text qualified as Text
import Data.Text (Text)
import Lang.Crucible.Debug.Arg (Arg)
import Lang.Crucible.Debug.Arg qualified as Arg
import Lang.Crucible.Debug.Arg.Type (ArgTypeRepr)
import Lang.Crucible.Debug.Breakpoint (Breakpoint)
import Lang.Crucible.Debug.Command (CommandExt)
import Lang.Crucible.Debug.Command qualified as Cmd
import Lang.Crucible.Debug.Regex qualified as Rgx
import Lang.Crucible.FunctionHandle qualified as C
import Lang.Crucible.Simulator.ExecutionTree qualified as C
import Prettyprinter qualified as PP
import System.Console.Isocline qualified as Isocline

data StyleEnv cExt
  = forall p sym ext r.
    StyleEnv
  { envBreakpoints :: Set Breakpoint
  , envCommandExt :: CommandExt cExt
  , envState :: C.ExecState p sym ext r
  }

newtype StyleT cExt m a
  = StyleT
    { runStyleT :: ReaderT (StyleEnv cExt) m a }
  deriving (Applicative, Functor, Monad, MonadFail, MonadIO, MonadReader (StyleEnv cExt), MonadTrans)

-- | Run 'StyleT' over 'Identity'
runStyle :: StyleEnv cExt -> StyleT cExt Identity a -> a
runStyle env s = runIdentity (Reader.runReaderT (runStyleT s) env)

-- | Run 'StyleT' over another monad
--
-- This is kind of poorly named, but I can\'t think of a better name...
runStyleM :: StyleEnv cExt -> StyleT cExt m a -> m a
runStyleM env s = Reader.runReaderT (runStyleT s) env

data Style
  = SBreakpoint
  | SCommand
  | SExactly
  | SFunction
  | SNumber
  | SPath
  | SText
  | SUnknown
  deriving (Eq, Show)

instance PP.Pretty Style where
  pretty =
    \case
      SBreakpoint -> "breakpoint"
      SCommand -> "command"
      SExactly -> "exactly"
      SFunction -> "function"
      SNumber -> "number"
      SPath -> "path"
      SText -> "text"
      SUnknown -> "unknown"

styleFor :: Arg cExt t -> Style
styleFor =
  \case
    Arg.ABreakpoint {} -> SBreakpoint
    Arg.ACommand {} -> SCommand
    Arg.AExactly {} -> SExactly
    Arg.AFunction {} -> SFunction
    Arg.AInt {} -> SNumber
    Arg.APath {} -> SPath
    Arg.AText {} -> SText

data Valid
  = Invalid
  | Valid
  | Unknown
  deriving (Eq, Show)

instance PP.Pretty Valid where
  pretty =
    \case
      Invalid -> "invalid"
      Valid -> "valid"
      Unknown -> "unknown"

boolToValid :: Bool -> Valid
boolToValid False = Invalid
boolToValid True = Valid

validate ::
  Monad m =>
  Arg cExt t ->
  StyleT cExt m Valid
validate =
  \case
    Arg.ACommand {} -> pure Valid
    Arg.ABreakpoint b ->
      boolToValid <$> Reader.asks (Set.member b . envBreakpoints)
    Arg.AExactly {} -> pure Valid
    Arg.AFunction fNm -> do
      StyleEnv { envState = execState } <- Reader.ask
      let binds = execState Lens.^. Lens.to C.execStateContext . C.functionBindings
      let hdls = C.handleMapToHandles (C.fnBindings binds)
      case List.find (\(C.SomeHandle hdl) -> C.handleName hdl == fNm) hdls of
        Nothing ->
          -- May just not yet be loaded/discovered
          pure Unknown
        Just {} -> pure Valid
    Arg.AInt {} -> pure Valid
    Arg.APath {} -> pure Valid
    Arg.AText {} -> pure Valid

data Styled
  = Styled
    { styledStyle :: Style
    , styledValid :: Valid
    }
  deriving (Eq, Show)

instance PP.Pretty Styled where
  pretty s =
    PP.pretty (styledStyle s) PP.<+> PP.parens (PP.pretty (styledValid s))

styled ::
  Monad m =>
  Arg cExt t ->
  StyleT cExt m Styled
styled a = Styled (styleFor a) <$> validate a

style ::
  Monad m =>
  Rgx.RegexRepr ArgTypeRepr r ->
  [Text] ->
  StyleT cExt m [Styled]
style r =
  \case
    [] -> pure []
    (word : ws) -> do
      cExt <- Reader.asks envCommandExt
      let bad = List.replicate (1 + length ws) (Styled SUnknown Invalid)
      case Arg.derivative cExt word r of
        Rgx.DerivativeResult (Some Rgx.Fail) _ -> pure bad
        Rgx.DerivativeResult (Some _) Seq.Empty -> pure bad
        Rgx.DerivativeResult (Some r') (Some m Seq.:<| Seq.Empty) -> do
          s <- styled m
          ss <- style r' ws
          pure (s : ss)
        Rgx.DerivativeResult (Some r') (Some m Seq.:<| ms) -> do
          s <- styled m
          ss <- mapM (viewSome styled) (toList ms)
          let combined = foldl' combineStyled s ss
          rest <- style r' ws
          pure (combined : rest)
  where
    combineStyles :: Style -> Style -> Style
    combineStyles s0 s = if s == s0 then s0 else SUnknown

    combineValid :: Valid -> Valid -> Valid
    combineValid v0 v = if v == v0 then v0 else Unknown

    combineStyled :: Styled -> Styled -> Styled
    combineStyled s0 s =
      Styled
      (combineStyles (styledStyle s0) (styledStyle s))
      (combineValid (styledValid s0) (styledValid s))

-- | Helper, not exported
badStyle :: String -> Isocline.Fmt
badStyle = Isocline.style "red"

-- | Helper, not exported
cmdStyle :: String -> Isocline.Fmt
cmdStyle = Isocline.style "bold"

highlightWithRegex ::
  Monad m =>
  Rgx.RegexRepr ArgTypeRepr r ->
  String ->
  StyleT cExt m Isocline.Fmt
highlightWithRegex r line = do
  let (initSpaces, rest) = splitOnSpaces line
  let wds = List.map fst rest
  styles <- style r (List.map Text.pack wds)
  let styledWords = zipWith ($) (map doStyle styles) wds
  let wordsWithSpaces =
        foldMap (uncurry (++)) (zip styledWords (List.map snd rest))
  pure (initSpaces ++ wordsWithSpaces)
  where
    splitOnSpaces :: String -> (String, [(String, String)])
    splitOnSpaces s =
      let (initSpaces, rest) = List.span (== ' ') s in
      (initSpaces, go rest)
      where
        go "" = []
        go x =
          let (word, rest) = List.span (/= ' ') x in
          let (spaces, rest') = List.span (== ' ') rest in
          (word, spaces) : go rest'

    -- Fairly arbitrary choices
    doStyle :: Styled -> String -> Isocline.Fmt
    doStyle s =
      case styledValid s of
        Invalid -> badStyle
        _ ->
          case styledStyle s of
            SBreakpoint -> Isocline.style "green"
            SCommand -> cmdStyle
            SExactly -> Isocline.style "keyword"
            SFunction -> Isocline.style "blue"
            SNumber -> Isocline.style "number"
            SPath -> Isocline.style "italic"
            SText -> id
            SUnknown -> id

highlighter :: Monad m => String -> StyleT cExt m Isocline.Fmt
highlighter input = do
  cExt <- Reader.asks envCommandExt
  let (initSpaces, rest) = List.span (== ' ') input
  let (cmdStr, rest') = List.span (/= ' ') rest
  case Cmd.parse cExt (Text.pack cmdStr) of
    Just cmd ->
      case Cmd.regex cExt cmd of
        Some rx -> do
          styledRest <- highlightWithRegex rx rest'
          pure $ List.concat [initSpaces , cmdStyle cmdStr, styledRest]
    Nothing -> pure $ List.concat [initSpaces, badStyle cmdStr, rest']