packages feed

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

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

For the sake of correctness, consistency, and concision, it is desirable to
declaratively specify the types of arguments expected by 'Cmd.Command's. These
specifications are used to:

* Parse said arguments (see 'match')
* Generate usage hints for @usage@ (see 'usage')
* Perform syntax highlighting (see "Lang.Crucible.Debug.Style")
* Provide tab-completions (see 'complete')

Perhaps the simplest workable argument specification would be a list of basic
types (e.g., integer, string). However, the purpose of the command language
is to be convenient for interactive use. In particular, users should not have
to remember the names of several variants of a command that do fundamentally
similar tasks. To provide sufficient flexibilty to unite such variants under
a single command, the specification should allow (in increasing order of
sophistication):

* sequencing (i.e., arity higher than 1),
* disjunction (at the very least, in a limited form), and
* quantification (optionality, repetition, and in particular, variable arity).

To justify the necessity of such features, consider the following examples of
each, taken from the bread-and-butter commands of the @gdb@ command language:

* sequencing: @break@ takes several (optional) arguments
* optionality: @step@ takes an optional integer argument
* quantification: @delete@ takes a variable number of breakpoint numbers

The basic list-of-types specification clearly lacks such features. Luckily,
there is a well-understood specification language that allows all of the above,
namely, regular expressions. Thus, the debugger\'s commands are specified
using the facilities provided by "Lang.Crucible.Debug.Regex". The tokens of
the regular expression are basic types (see 'ArgTypeRepr'). Operations like
'derivative' are used internally to provide the high-level functionality
described at the beginning of this documentation.

Commands that are not well-suited to this kind of argument validation can simply
accept @TEXT*@ and perform their own parsing (or not) at the cost of lacking
helpful documentation and completion. This is exactly the specification of the
comment (@#@) command, for example.
-}

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}

module Lang.Crucible.Debug.Arg
  ( Arg(..)
  -- * Parsers
  , ArgParseError(..)
  , ArgParser(..)
  , convert
  -- * Operations
  , match
  , derivative
  , types
  , usage
  ) where

import Data.Coerce (coerce)
import Data.Foldable qualified as Foldable
import Data.Kind (Type)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Parameterized.Some (Some(Some))
import Data.Parameterized.SymbolRepr (SymbolRepr, symbolRepr)
import Data.Parameterized.TraversableFC (fmapFC)
import Data.Sequence (Seq)
import Data.Text qualified as Text
import Lang.Crucible.Debug.Arg.Type (type ArgType(..), ArgTypeRepr(..))
import Lang.Crucible.Debug.Breakpoint (Breakpoint(BreakFunction))
import Lang.Crucible.Debug.Command qualified as Cmd
import Lang.Crucible.Debug.Regex qualified as Rgx
import Prelude hiding (Int)
import Prelude qualified as Prelude
import Prettyprinter qualified as PP
import Text.Read (readMaybe)
import What4.FunctionName qualified as W4

type Arg :: Type -> ArgType -> Type
data Arg cExt i where
  ABreakpoint :: Breakpoint -> Arg cExt 'TBreakpoint
  ACommand :: Cmd.Command cExt -> Arg cExt 'TCommand
  AExactly :: SymbolRepr s -> Arg cExt ('TExactly s)
  AFunction :: W4.FunctionName -> Arg cExt 'TFunction
  AInt :: Prelude.Int -> Arg cExt 'TInt
  APath :: Text.Text -> Arg cExt 'TPath
  AText :: Text.Text -> Arg cExt 'TText

data ArgParseError
  = NotACommand Text.Text
  | NotAnInt Text.Text
  | NotExactly Text.Text Text.Text
  deriving Show

instance PP.Pretty ArgParseError where
  pretty =
    \case
      NotACommand t -> PP.squotes (PP.pretty t) PP.<+> "is not a command"
      NotAnInt t -> PP.squotes (PP.pretty t) PP.<+> "is not an integer"
      NotExactly e t -> PP.squotes (PP.pretty t) PP.<+> "is not" PP.<+> PP.squotes (PP.pretty e)

newtype ArgParser cExt t
  = ArgParser { _runArgParser :: Rgx.TokenParser Text.Text ArgParseError (Arg cExt) t }

argParser :: (Text.Text -> Either ArgParseError (Arg cExt t)) -> ArgParser cExt t
argParser = ArgParser . Rgx.TokenParser

-- | Helper, not exported
maybeToEither :: e -> Maybe a -> Either e a
maybeToEither e =
  \case
    Nothing -> Left e
    Just a -> Right a

breakpoint :: ArgParser cExt 'TBreakpoint
breakpoint =
  argParser (Right . ABreakpoint . BreakFunction . W4.functionNameFromText)

function :: ArgParser cExt 'TFunction
function =
  argParser (Right . AFunction .  W4.functionNameFromText)

command :: Cmd.CommandExt cExt -> ArgParser cExt 'TCommand
command cExt = argParser (\t -> ACommand <$> maybeToEither (NotACommand t) (Cmd.parse cExt t))

exactly :: SymbolRepr s -> ArgParser cExt ('TExactly s)
exactly s =
  argParser $ \t ->
    if t == symbolRepr s
    then Right (AExactly s)
    else Left (NotExactly (symbolRepr s) t)

int :: ArgParser cExt 'TInt
int = argParser (\t -> AInt <$> maybeToEither (NotAnInt t) (readMaybe (Text.unpack t)))

path :: ArgParser cExt 'TPath
path = argParser (Right . APath)

text :: ArgParser cExt 'TText
text = argParser (Right . AText)

parser :: Cmd.CommandExt cExt -> ArgTypeRepr t -> ArgParser cExt t
parser cExt =
  \case
    TBreakpointRepr -> breakpoint
    TCommandRepr -> command cExt
    TExactlyRepr s -> exactly s
    TFunctionRepr -> function
    TIntRepr -> int
    TPathRepr -> path
    TTextRepr -> text

convert ::
  Cmd.CommandExt cExt ->
  Rgx.RegexRepr ArgTypeRepr r ->
  Rgx.RegexRepr (ArgParser cExt) r
convert cExt = fmapFC (parser cExt)

match ::
  forall r cExt.
  Rgx.RegexRepr (ArgParser cExt) r ->
  [Text.Text] ->
  Either (Rgx.MatchError Text.Text ArgParseError) (Rgx.Match (Arg cExt) r)
match r toks =
  case Rgx.match @_ @_ @(Arg cExt) (coerce r) toks of
    Left e -> Left e
    Right (m, []) -> Right m
    Right (_, as) -> Left (Rgx.NotEmpty as)

derivative ::
  forall cExt r.
  Cmd.CommandExt cExt ->
  Text.Text ->
  Rgx.RegexRepr ArgTypeRepr r ->
  Rgx.DerivativeResult ArgTypeRepr (Arg cExt)
derivative cExt =
  Rgx.derivative
    (coerce @_ @(Rgx.TokenParser _ ArgParseError (Arg cExt) _) . parser cExt)

-- | Get the potential types that the regex assigns each argument, plus what it
-- would assign to the next one.
types ::
  forall r cExt.
  Cmd.CommandExt cExt ->
  Rgx.RegexRepr ArgTypeRepr r ->
  [Text.Text] ->
  (Map Text.Text (Seq (Some ArgTypeRepr)), Seq (Some ArgTypeRepr))
types cExt r =
  \case
    [] -> (Map.empty, Rgx.nextLits r)
    (t : ts) ->
      case derivative cExt t r of
        Rgx.DerivativeResult (Some r') _ ->
          let (m, final) = types cExt r' ts in
          (Map.insertWith (<>) t (Rgx.nextLits r) m, final)

usage :: Rgx.RegexRepr ArgTypeRepr r -> [PP.Doc ann]
usage r =
  Foldable.toList $
    fmap (\(Some r') -> Rgx.prettySugar " " PP.pretty r') (Rgx.liftOrs (Rgx.sugar r))