crucible-debug-0.1.0: src/Lang/Crucible/Debug/Complete.hs
{-|
Copyright : (c) Galois, Inc. 2025
Maintainer : Langston Barrett <langston@galois.com>
-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeApplications #-}
module Lang.Crucible.Debug.Complete
( CompletionEnv(..)
, CompletionT
, runCompletionT
, runCompletion
, runCompletionM
, CompletionType(..)
, Completions(..)
, complete
) where
import Control.Lens qualified as Lens
import Control.Monad qualified as Monad
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 Control.Exception qualified as X
import Data.Foldable (toList)
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Functor.Identity (Identity, runIdentity)
import Data.List qualified as List
import Data.Map.Strict qualified as Map
import Data.Maybe qualified as Maybe
import Data.Parameterized.Some (Some(Some), viewSome)
import Data.Parameterized.SymbolRepr (symbolRepr)
import Data.Set (Set)
import Data.Text qualified as Text
import Data.Text (Text)
import Lang.Crucible.Debug.Arg qualified as Arg
import Lang.Crucible.Debug.Arg.Type (ArgTypeRepr)
import Lang.Crucible.Debug.Arg.Type qualified as AType
import Lang.Crucible.Debug.Breakpoint qualified as Break
import Lang.Crucible.Debug.Breakpoint (Breakpoint)
import Lang.Crucible.Debug.Command (CommandExt)
import Lang.Crucible.Debug.Command qualified as Cmd
import Lang.Crucible.FunctionHandle qualified as C
import Lang.Crucible.Simulator.ExecutionTree qualified as C
import Prettyprinter qualified as PP
import System.Directory qualified as Dir
import System.FilePath qualified as FilePath
import What4.FunctionName qualified as W4
data CompletionEnv cExt
= forall p sym ext r.
CompletionEnv
{ envBreakpoints :: Set Breakpoint
, envCommandExt :: CommandExt cExt
, envState :: C.ExecState p sym ext r
}
newtype CompletionT cExt m a
= CompletionT { runCompletionT :: ReaderT (CompletionEnv cExt) m a }
deriving (Applicative, Functor, Monad, MonadFail, MonadIO, MonadReader (CompletionEnv cExt), MonadTrans)
-- | Run 'CompletionT' over 'Identity'
runCompletion :: CompletionEnv cExt -> CompletionT cExt Identity a -> a
runCompletion env s = runIdentity (Reader.runReaderT (runCompletionT s) env)
-- | Run 'CompletionT' over another monad
--
-- This is kind of poorly named, but I can\'t think of a better name...
runCompletionM :: CompletionEnv cExt -> CompletionT cExt m a -> m a
runCompletionM env s = Reader.runReaderT (runCompletionT s) env
data CompletionType
= CBreakpoint
| CCommand
| CExactly Text
| CFunction
| CPath
deriving (Eq, Show)
instance PP.Pretty CompletionType where
pretty =
\case
CBreakpoint -> "breakpoint"
CCommand -> "command"
CExactly t -> PP.pretty t
CFunction -> "function"
CPath -> "path"
completionType :: ArgTypeRepr t -> Maybe CompletionType
completionType =
\case
AType.TBreakpointRepr -> Just CBreakpoint
AType.TCommandRepr -> Just CCommand
AType.TExactlyRepr s -> Just (CExactly (symbolRepr s))
AType.TFunctionRepr -> Just CFunction
AType.TIntRepr -> Nothing
AType.TPathRepr -> Just CPath
AType.TTextRepr -> Nothing
commandsWithPrefix ::
CompletionEnv cExt ->
-- | Prefix
String ->
[String]
commandsWithPrefix cEnv pfx = filter (pfx `List.isPrefixOf`) allCmdNames
where
allCmdNames :: [String]
allCmdNames =
let cExts = envCommandExt cEnv in
map (Text.unpack . Cmd.name cExts) (Cmd.allCmds cExts)
completePath :: FilePath -> IO [String]
completePath path =
if List.null path
then Dir.listDirectory =<< Dir.getCurrentDirectory
else do
isDir <- Dir.doesDirectoryExist path
if isDir && not (FilePath.hasTrailingPathSeparator path)
then pure [FilePath.addTrailingPathSeparator path]
else do
let dir = FilePath.addTrailingPathSeparator (FilePath.takeDirectory path)
contents <- Dir.listDirectory dir
let file = FilePath.takeFileName path
if List.null file
then pure (map (dir ++) contents)
else pure (map (dir ++) (filter (file `List.isPrefixOf`) contents))
completionsForType ::
CompletionEnv cExt ->
-- | Prefix
String ->
CompletionType ->
IO [String]
completionsForType cEnv pfx =
\case
CBreakpoint ->
pure $
toList (envBreakpoints cEnv) &
map Break.toText &
filter (Text.pack pfx `Text.isPrefixOf`) &
map Text.unpack
CCommand -> pure (commandsWithPrefix cEnv pfx)
CExactly s ->
let s' = Text.unpack s in
pure (if pfx `List.isPrefixOf` s' then [s'] else [])
CFunction -> do
CompletionEnv { envState = execState } <- pure cEnv
let binds = execState Lens.^. Lens.to C.execStateContext . C.functionBindings
let hdls = C.handleMapToHandles (C.fnBindings binds)
pure (map (\(C.SomeHandle hdl) -> Text.unpack (W4.functionName (C.handleName hdl))) hdls)
CPath ->
X.try @X.IOException (completePath pfx) <&>
\case
Left _ -> []
Right cs -> cs
data Completions
= Completions
{ completionsType :: CompletionType
, completions :: [String]
}
-- | Used in the test suite with @. complete@
instance PP.Pretty Completions where
pretty (Completions ty cs) = do
let def =
if null cs
then PP.pretty ty
else
PP.pretty ty PP.<> ":" PP.<+> PP.hsep (PP.punctuate "," (map PP.pretty cs))
case ty of
CBreakpoint -> def
CCommand -> PP.pretty ty
CExactly {} ->
if null cs
then PP.pretty ty PP.<+> "(x)"
else PP.pretty ty
CFunction {} -> def
CPath -> def
complete ::
CompletionEnv cExt ->
-- | Whole line
String ->
-- | Word being completed
String ->
IO [Completions]
complete cEnv input word =
case words input of
[] -> pure [Completions CCommand allCmdNames]
[_] | not (null word) ->
pure [Completions CCommand (commandsWithPrefix cEnv word)]
(c : args) -> do
case Cmd.parse cExts (Text.pack c) of
Just cmd ->
case Cmd.regex cExts cmd of
Some rx -> do
let (allTypes, next) = Arg.types cExts rx (map Text.pack args)
let types =
case Map.lookup (Text.pack word) allTypes of
Nothing -> next
Just ts -> ts
let cTypes = Maybe.mapMaybe (viewSome completionType) (toList types)
Monad.mapM (\t -> Completions t <$> completionsForType cEnv word t) cTypes
Nothing -> pure []
where
cExts = envCommandExt cEnv
allCmdNames :: [String]
allCmdNames = map (Text.unpack . Cmd.name cExts) (Cmd.allCmds cExts)