packages feed

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

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

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}

module Lang.Crucible.Debug.Commands
  ( execStateSimState
  , backtrace
  , block
  , call
  , cfg
  , frame
  , help
  , load
  , prove
  , reg
  , secret
  , source
  , usage
  ) where

import Control.Exception qualified as X
import Control.Lens qualified as Lens
import Control.Monad.Except (runExceptT)
import Control.Monad.IO.Class (liftIO)
import Data.List qualified as List
import Data.Maybe qualified as Maybe
import Data.Parameterized.Classes (ixF')
import Data.Parameterized.Context qualified as Ctx
import Data.Parameterized.Nonce qualified as Nonce
import Data.Parameterized.Some (Some(Some), viewSome)
import Data.Parameterized.TraversableFC (toListFC)
import Data.Sequence qualified as Seq
import Data.Text.IO qualified as IO
import Data.Text qualified as Text
import Lang.Crucible.Analysis.Postdom qualified as C
import Lang.Crucible.Backend.Prove qualified as Prove
import Lang.Crucible.Backend qualified as C
import Lang.Crucible.CFG.Core qualified as C
import Lang.Crucible.CFG.Extension qualified as C
import Lang.Crucible.CFG.Reg qualified as C.Reg
import Lang.Crucible.CFG.SSAConversion qualified as C
import Lang.Crucible.Debug.Arg qualified as Arg
import Lang.Crucible.Debug.Command.Base qualified as BCmd
import Lang.Crucible.Debug.Command qualified as Cmd
import Lang.Crucible.Debug.Complete (CompletionT)
import Lang.Crucible.Debug.Complete qualified as Complete
import Lang.Crucible.Debug.Context (Context)
import Lang.Crucible.Debug.Context qualified as Ctxt
import Lang.Crucible.Debug.Inputs (Inputs)
import Lang.Crucible.Debug.Inputs qualified as Inps
import Lang.Crucible.Debug.Outputs qualified as Outps
import Lang.Crucible.Debug.Regex qualified as Rgx
import Lang.Crucible.Debug.Response qualified as Resp
import Lang.Crucible.Debug.Response (Response)
import Lang.Crucible.Debug.Statement qualified as Stmt
import Lang.Crucible.Debug.Statement (Statement)
import Lang.Crucible.Debug.Style qualified as Style
import Lang.Crucible.Debug.Style (StyleT)
import Lang.Crucible.FunctionHandle qualified as C
import Lang.Crucible.Pretty (ppRegVal)
import Lang.Crucible.Simulator.CallFrame qualified as C
import Lang.Crucible.Simulator.EvalStmt qualified as C
import Lang.Crucible.Simulator.ExecutionTree qualified as C
import Lang.Crucible.Simulator qualified as C
import Lang.Crucible.Syntax.Atoms qualified as C
import Lang.Crucible.Syntax.Concrete qualified as C
import Lang.Crucible.Syntax.SExpr qualified as C
import Lang.Crucible.Utils.Seconds qualified as Sec
import Lang.Crucible.Utils.Timeout qualified as CTO
import Prettyprinter qualified as PP
import System.Exit qualified as Exit
import Text.Megaparsec qualified as MP
import What4.Config qualified as W4
import What4.Expr.Builder qualified as W4
import What4.FunctionName qualified as W4
import What4.Interface qualified as W4
import What4.ProgramLoc qualified as W4
import What4.Solver qualified as W4

---------------------------------------------------------------------
-- Helpers, exported

execStateSimState ::
  C.ExecState p sym ext r ->
  Either Resp.NotApplicable (C.SomeSimState p sym ext r)
execStateSimState =
  \case
    C.ResultState _                  -> Left Resp.DoneSimulating
    C.AbortState _ st                -> Right (C.SomeSimState st)
    C.UnwindCallState _ _ st         -> Right (C.SomeSimState st)
    C.CallState _ _ st               -> Right (C.SomeSimState st)
    C.TailCallState _ _ st           -> Right (C.SomeSimState st)
    C.ReturnState _ _ _ st           -> Right (C.SomeSimState st)
    C.ControlTransferState _ st      -> Right (C.SomeSimState st)
    C.RunningState _ st              -> Right (C.SomeSimState st)
    C.SymbolicBranchState _ _ _ _ st -> Right (C.SomeSimState st)
    C.OverrideState _ st             -> Right (C.SomeSimState st)
    C.BranchMergeState _ st          -> Right (C.SomeSimState st)
    C.InitialState _ _ _ _ _         -> Left Resp.NotYetSimulating

---------------------------------------------------------------------
-- Helpers, not exported

insertCfg ::
  C.IsSyntaxExtension ext =>
  C.Reg.CFG ext blocks init ret ->
  C.FnHandleMap (C.FnState p sym ext) ->
  C.FnHandleMap (C.FnState p sym ext)
insertCfg regCFG hdlMap =
  case C.toSSA regCFG of
    C.SomeCFG ssaCFG ->
      C.insertHandleMap
        (C.cfgHandle ssaCFG)
        (C.UseCFG ssaCFG (C.postdomInfo ssaCFG))
        hdlMap

data SomeFn f = forall args ret. SomeFn (f args ret)

lookupHandleMapByName ::
  W4.FunctionName ->
  C.FnHandleMap f ->
  Maybe (SomeFn f)
lookupHandleMapByName fNm hdls = do
  let sHdls = C.handleMapToHandles hdls
  C.SomeHandle h <- List.find (\(C.SomeHandle hdl) -> C.handleName hdl == fNm) sHdls
  SomeFn <$> C.lookupHandleMap h hdls

-- TODO: Upstream to Crucible as a Lens
modifySimContext ::
  (C.SimContext p sym ext -> C.SimContext p sym ext) ->
  C.ExecState p sym ext r ->
  C.ExecState p sym ext r
modifySimContext f =
  \case
    C.ResultState r ->
      case r of
        C.FinishedResult ctx x -> C.ResultState (C.FinishedResult (f ctx) x)
        C.AbortedResult ctx x -> C.ResultState (C.AbortedResult (f ctx) x)
        C.TimeoutResult es -> modifySimContext f es
    C.AbortState x s -> C.AbortState x (s Lens.& C.stateContext Lens.%~ f)
    C.UnwindCallState x y s -> C.UnwindCallState x y (s Lens.& C.stateContext Lens.%~ f)
    C.CallState x y s -> C.CallState x y (s Lens.& C.stateContext Lens.%~ f)
    C.TailCallState x y s -> C.TailCallState x y (s Lens.& C.stateContext Lens.%~ f)
    C.ReturnState x y z s -> C.ReturnState x y z (s Lens.& C.stateContext Lens.%~ f)
    C.ControlTransferState x s -> C.ControlTransferState x (s Lens.& C.stateContext Lens.%~ f)
    C.RunningState x s -> C.RunningState x (s Lens.& C.stateContext Lens.%~ f)
    C.SymbolicBranchState x y z w s -> C.SymbolicBranchState x y z w (s Lens.& C.stateContext Lens.%~ f)
    C.OverrideState x s -> C.OverrideState x (s Lens.& C.stateContext Lens.%~ f)
    C.BranchMergeState x s -> C.BranchMergeState x (s Lens.& C.stateContext Lens.%~ f)
    C.InitialState ctx x y z w -> C.InitialState (f ctx) x y z w

---------------------------------------------------------------------
-- Commands, exported

backtrace ::
  C.ExecState p sym ext r ->
  IO (Response cExt)
backtrace execState =
  case execStateSimState execState of
    Left notApplicable -> pure (Resp.UserError (Resp.NotApplicable notApplicable))
    Right (C.SomeSimState simState) -> do
      let frames = C.activeFrames (C._stateTree simState)
      pure (Resp.Backtrace (C.ppExceptionContext frames))

block ::
  C.IsSyntaxExtension ext =>
  W4.IsExpr (W4.SymExpr sym) =>
  Context cExt p sym ext t ->
  C.ExecState p sym ext r ->
  IO (Response cExt)
block _ctx execState =
  case execStateSimState execState of
    Left notApplicable -> pure (Resp.UserError (Resp.NotApplicable notApplicable))
    Right (C.SomeSimState simState) ->
      let fr = simState Lens.^. C.stateTree . C.actFrame . C.gpValue in
      case fr of
        C.MF callFrame ->
          let regs = C.regMap (callFrame Lens.^. C.frameRegs) in
          pure (Resp.Block (ppStmtSeq False (Ctx.size regs) (C._frameStmts callFrame)))
        C.OF {} -> pure (Resp.UserError (Resp.NotApplicable Resp.NoCfg))
        C.RF {} -> pure (Resp.UserError (Resp.NotApplicable Resp.NoCfg))
  where
    -- TODO: The below is taken from Crucible. Export?

    prefixLineNum :: Bool -> W4.ProgramLoc -> PP.Doc ann -> PP.Doc ann
    prefixLineNum True pl d = PP.vcat ["%" PP.<+> W4.ppNoFileName (W4.plSourceLoc pl), d]
    prefixLineNum False _ d = d

    ppStmtSeq :: C.PrettyExt ext => Bool -> Ctx.Size ctx -> C.StmtSeq ext blocks ret ctx -> PP.Doc ann
    ppStmtSeq ppLineNum h (C.ConsStmt pl s r) =
      PP.vcat
      [ prefixLineNum ppLineNum pl (C.ppStmt h s)
      , ppStmtSeq ppLineNum (C.nextStmtHeight h s) r
      ]
    ppStmtSeq ppLineNum _ (C.TermStmt pl s) =
      prefixLineNum ppLineNum pl (PP.pretty s)

call ::
  C.IsSymInterface sym =>
  C.IsSyntaxExtension ext =>
  C.ExecState p sym ext (C.RegEntry sym t) ->
  C.TypeRepr t ->
  W4.FunctionName ->
  IO (C.ExecutionFeatureResult p sym ext (C.RegEntry sym t), Response cExt)
call execState rTy fNm = do
  let fTy = C.FunctionHandleRepr Ctx.empty C.UnitRepr
  let binds = execState Lens.^. Lens.to C.execStateContext . C.functionBindings
  case C.searchHandleMap fNm fTy (C.fnBindings binds) of
    Nothing -> do
      let hdls = C.handleMapToHandles (C.fnBindings binds)
      let names = map (\(C.SomeHandle hdl) -> C.handleName hdl) hdls
      pure (C.ExecutionFeatureNoChange, Resp.UserError (Resp.FnNotFound fNm names))
    Just (hdl, bind) -> do
      case execStateSimState execState of
        Left notApplicable -> pure (C.ExecutionFeatureNoChange, Resp.UserError (Resp.NotApplicable notApplicable))
        Right (C.SomeSimState simState) -> do
          let initCtx = simState Lens.^. C.stateContext
          let globs = simState Lens.^. C.stateGlobals
          let abortHdl = simState Lens.^. C.abortHandler
          let initState =
                C.InitialState initCtx globs abortHdl rTy $
                  C.runOverrideSim rTy $
                    let args = C.RegMap Ctx.empty in
                    case bind of
                      C.UseOverride o -> do
                        _ <- C.callOverride hdl o args
                        C.exitExecution Exit.ExitSuccess
                      C.UseCFG c _ -> do
                        _ <- C.callCFG c args
                        C.exitExecution Exit.ExitSuccess
          let featRes = C.ExecutionFeatureNewState initState
          pure (featRes, Resp.Ok)

cfg ::
  C.IsSyntaxExtension ext =>
  C.ExecState p sym ext r ->
  Maybe W4.FunctionName ->
  IO (Response cExt)
cfg execState =
  \case
    Just fNm -> do
      let binds = execState Lens.^. Lens.to C.execStateContext . C.functionBindings
      let hdls = C.handleMapToHandles (C.fnBindings binds)
      let names = map (\(C.SomeHandle hdl) -> C.handleName hdl) hdls
      case lookupHandleMapByName fNm (C.fnBindings binds) of
        Nothing -> pure (Resp.UserError (Resp.FnNotFound fNm names))
        Just (SomeFn (C.UseOverride {})) -> pure (Resp.UserError (Resp.NotACfg fNm))
        Just (SomeFn (C.UseCFG c _)) -> pure (Resp.Cfg (C.ppCFG True c))
    Nothing ->
      case execStateSimState execState of
        Left notApplicable -> pure (Resp.UserError (Resp.NotApplicable notApplicable))
        Right (C.SomeSimState simState) -> do
          case simState Lens.^. C.stateTree . C.actFrame . C.gpValue of
            C.MF (C.CallFrame { C._frameCFG = c }) ->
              pure (Resp.Cfg (C.ppCFG True c))
            C.OF {} -> pure (Resp.UserError (Resp.NotApplicable Resp.NoCfg))
            C.RF {} -> pure (Resp.UserError (Resp.NotApplicable Resp.NoCfg))

frame ::
  C.IsSyntaxExtension ext =>
  W4.IsExpr (W4.SymExpr sym) =>
  Context cExt p sym ext t ->
  C.ExecState p sym ext r ->
  IO (Response cExt)
frame _ctx execState =
  case execStateSimState execState of
    Left notApplicable -> pure (Resp.UserError (Resp.NotApplicable notApplicable))
    Right (C.SomeSimState simState) ->
      pure $ Resp.Frame $
        let fr = simState Lens.^. C.stateTree . C.actFrame . C.gpValue in
        let name = fr Lens.^. C.frameFunctionName in
        case fr of
          C.OF ovFrame ->
            let regs = C.regMap (ovFrame Lens.^. C.overrideRegMap) in
            PP.vcat
            [ "Override:" PP.<+> PP.pretty name
            , ""
            , "Argument types:"
            , PP.vcat $ toListFC (bullet . PP.pretty . C.regType) regs
            ]
          C.MF callFrame ->
            let regs = C.regMap (callFrame Lens.^. C.frameRegs) in
            PP.vcat
              [ "CFG:" PP.<+> PP.pretty name
              , "Block:" PP.<+> viewSome PP.pretty (C._frameBlockID callFrame)
              , ""
              , "Argument types:"
              , PP.vcat $ toListFC (bullet . PP.pretty . C.regType) regs
              ]
          C.RF {} -> "Return:" PP.<+> PP.pretty name
  where bullet = ("-" PP.<+>) . PP.align

help ::
  C.IsSyntaxExtension ext =>
  (?parserHooks :: C.ParserHooks ext) =>
  PP.Pretty cExt =>
  Cmd.CommandExt cExt ->
  Maybe (Cmd.Command cExt) ->
  Response cExt
help cExts =
  \case
    Nothing ->
      let mkHelp c = Cmd.name cExts c <> " (" <> Cmd.abbrev cExts c <> ")" <> ": " <> Cmd.help cExts c in
      Resp.Help (PP.pretty (Text.unlines (List.sort (map mkHelp (Cmd.allCmds cExts)))))
    Just cmd -> 
      Resp.Help $
        -- See 'Lang.Crucible.Debug.Command.ext{Detail,Help}' for expectations
        -- on these strings, which are why it makes sense to format them this
        -- way (e.g., adding a period, printing "details" after "help"), etc.
        let basic = [usage cExts cmd, "", PP.pretty (Cmd.help cExts cmd) <> "."]
            detail =
              case Cmd.detail cExts cmd of
                Just more -> ["", PP.pretty more]
                Nothing -> []
        in PP.vcat (basic ++ detail)

load ::
  C.IsSyntaxExtension ext =>
  (?parserHooks :: C.ParserHooks ext) =>
  C.ExecState p sym ext r ->
  FilePath ->
  IO (C.ExecutionFeatureResult p sym ext r, Response cExt)
load execState0 path = do
  eTxt <- X.try (IO.readFile path)
  case eTxt of
    Left ioErr -> pure (C.ExecutionFeatureNoChange, Resp.IOError ioErr)
    Right txt -> do
      case MP.parse (C.skipWhitespace *> MP.many (C.sexp C.atom) <* MP.eof) path txt of
        Left err -> pure (C.ExecutionFeatureNoChange, Resp.UserError (Resp.LoadParseError err))
        Right v ->
          Nonce.withIONonceGenerator $ \ng -> do
            let execCtx = C.execStateContext execState0
            let ha = C.simHandleAllocator execCtx
            parseResult <- C.top ng ha [] $ C.prog v
            case parseResult of
              Left err ->
                pure (C.ExecutionFeatureNoChange, Resp.UserError (Resp.LoadSyntaxError (PP.pretty err)))
              Right (C.ParsedProgram{ C.parsedProgCFGs = cfgs }) -> do
                let binds = execState0 Lens.^. Lens.to C.execStateContext . C.functionBindings . Lens.to C.fnBindings
                let binds' = foldl (\bs (C.Reg.AnyCFG c) -> insertCfg c bs) binds cfgs
                let execState = modifySimContext (\c -> c Lens.& C.functionBindings Lens..~ C.FnBindings binds') execState0
                let featRes = C.ExecutionFeatureModifiedState execState
                pure (featRes, Resp.Load path (length cfgs))

prove ::
  (sym ~ W4.ExprBuilder t st fs) =>
  C.IsSymBackend sym bak =>
  bak ->
  IO (Response cExt)
prove bak = do
  let sym = C.backendGetSym bak
  -- TODO(#270?): Configurable timeout
  let timeout = CTO.Timeout (Sec.secondsFromInt 5)
  -- TODO(#266): Configurable prover
  W4.extendConfig W4.z3Options (W4.getConfiguration sym)
  let prover = Prove.offlineProver timeout sym W4.defaultLogData W4.z3Adapter
  let strat = Prove.ProofStrategy prover Prove.keepGoing
  let convertResult =
        \case
          Prove.Proved {} -> Resp.Proved
          Prove.Disproved {} -> Resp.Disproved
          Prove.Unknown {} -> Resp.Unknown
  let printer = Prove.ProofConsumer $ \goal res -> do
        doc <- C.ppProofObligation sym goal
        pure (Seq.fromList [(doc, convertResult res)])
  runExceptT (Prove.proveCurrentObligations bak strat printer) >>=
    \case
      Left CTO.TimedOut -> pure Resp.ProveTimeout
      Right results -> pure (Resp.Prove results)

-- TODO: Support taking a function or block name
reg ::
  forall p sym ext r t cExt.
  W4.IsExpr (W4.SymExpr sym) =>
  Context cExt p sym ext t ->
  C.ExecState p sym ext r ->
  [Int] ->
  IO (Response cExt)
reg ctx execState idxs =
  case execStateSimState execState of
    Left notApplicable -> pure (Resp.UserError (Resp.NotApplicable notApplicable))
    Right (C.SomeSimState simState) -> do
      let fr = simState Lens.^. C.stateTree . C.actFrame . C.gpValue
      let regs :: Maybe (C.Some (C.RegMap sym))
          regs =
            case fr of
              C.OF ovFrame ->
                Just (C.Some (ovFrame Lens.^. C.overrideRegMap))
              C.MF callFrame ->
                Just (C.Some (callFrame Lens.^. C.frameRegs))
              C.RF {} ->
                Nothing
      case regs of
        Nothing -> pure (Resp.UserError (Resp.NotApplicable Resp.NoFrame))
        Just (C.Some (C.RegMap rs)) -> do
          let sz = Ctx.size rs
          let mIdxs =
                if List.null idxs
                then Right (toListFC Some (Ctx.generate sz id))  -- all indices
                else traverse (\i -> maybeToEither i (Ctx.intIndex i sz)) idxs
          let ppArg (C.Some idx) =
                let C.RegEntry ty val = rs Lens.^. ixF' idx in
                ppRegVal (Ctxt.dbgPpIntrinsic ctx) ty (C.RV val)
          case mIdxs of
            Left i -> pure (Resp.UserError (Resp.NoSuchArgument i))
            Right idxs' -> pure (Resp.Arg (PP.vcat (List.map ppArg idxs')))
  where
    maybeToEither :: e -> Maybe a -> Either e a
    maybeToEither e =
      \case
        Nothing -> Left e
        Just a -> Right a

secret ::
  PP.Pretty cExt =>
  Context cExt p sym ext t ->
  C.ExecState p sym ext r ->
  Rgx.Match (Arg.Arg cExt) BCmd.SecretRegex ->
  IO (Resp.Response cExt)
secret ctx execState m =
  let cExt = Ctxt.dbgCommandExt ctx in
  case m of
    -- Show what sort of tab-completion should be provided by the UI
    Rgx.MLeft (Rgx.MThen _complete (Rgx.MThen (Rgx.MLit (Arg.ACommand cmd)) (Rgx.MStar args0))) -> do
      let args = map (\(Rgx.MLit (Arg.AText t)) -> Text.unpack t) args0
      let cEnv = Ctxt.toCompletionEnv ctx execState
      let line = unwords (show (PP.pretty cmd) : map (filter (/= '_')) args)
      -- Act like we are completing as if the user pressed TAB at the "_"
      let cursor = Maybe.fromMaybe "_" (List.find ("_" `List.isSuffixOf`) args)
      completions <- Complete.complete cEnv line (List.init cursor)
      pure $
        Resp.Complete $
          case completions of
            [] -> "No completions"
            _ -> PP.vcat (List.map PP.pretty completions)
    -- Show what sort of syntax-highlighting should be provided by the UI
    Rgx.MRight (Rgx.MThen _secret (Rgx.MThen (Rgx.MLit (Arg.ACommand cmd)) (Rgx.MStar args0))) ->
      let args = map (\(Rgx.MLit (Arg.AText t)) -> t) args0 in
      case Cmd.regex cExt cmd of
        Some rx ->
          let sEnv = Ctxt.toStyleEnv ctx execState in
          pure (Resp.Style (Style.runStyle sEnv (Style.style rx args)))

source ::
  Context cExt p sym ext t ->
  FilePath ->
  IO (Either X.IOException (Inputs (CompletionT cExt (StyleT cExt IO)) (Statement cExt)))
source ctx path = do
  let cExt = Ctxt.dbgCommandExt ctx
  eTxt <- liftIO (X.try (IO.readFile path))
  case eTxt of
    Left ioErr -> pure (Left ioErr)
    Right txt -> do
      let lns = Text.lines txt
      let cmds = traverse (Stmt.parse cExt) lns
      case cmds of
        Left err -> do
          Outps.send (Ctxt.dbgOutputs ctx) (Resp.UserError (Resp.SourceParseError err))
          pure (Right (Ctxt.dbgInputs ctx))
        Right stmts -> Right <$> Inps.prepend stmts (Ctxt.dbgInputs ctx)

usage ::
  PP.Pretty cExt =>
  Cmd.CommandExt cExt ->
  Cmd.Command cExt ->
  PP.Doc ann
usage cExt cmd =
  case Cmd.regex cExt cmd of
    Some rx -> PP.vcat (map (PP.pretty cmd PP.<+>) (Arg.usage rx))