packages feed

hevm-0.51.2: src/EVM/TTY.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ImplicitParams #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}

module EVM.TTY where

import Brick
import Brick.Widgets.Border
import Brick.Widgets.Center
import Brick.Widgets.List

import EVM
import EVM.ABI (decodeAbiValue, emptyAbi, abiTypeSolidity, AbiType(..))
import EVM.SymExec (maxIterationsReached, symCalldata)
import EVM.Expr (simplify)
import EVM.Dapp (DappInfo(..), emptyDapp, dappInfo, Test, extractSig, Test(..), srcMap, unitTestMethods)
import EVM.Debug
import EVM.Fetch (Fetcher)
import EVM.Fetch qualified as Fetch
import EVM.Format (showWordExact, showWordExplanation, contractNamePart,
  contractPathPart, showTraceTree, prettyIfConcreteWord, formatExpr)
import EVM.Hexdump (prettyHex)
import EVM.Solvers (SolverGroup)
import EVM.Op
import EVM.Solidity hiding (storageLayout)
import EVM.Types hiding (padRight, Max)
import EVM.UnitTest
import EVM.Stepper (Stepper)
import EVM.Stepper qualified as Stepper
import EVM.StorageLayout
import EVM.TTYCenteredList qualified as Centered

import Optics.Core
import Optics.State
import Optics.TH

import Control.Monad.Operational qualified as Operational
import Control.Monad.State.Strict hiding (state)
import Data.Aeson.Optics
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.List (sort, find)
import Data.Maybe (isJust, fromJust, fromMaybe, isNothing)
import Data.Map (Map, insert, lookupLT, singleton, filter, (!?))
import Data.Map qualified as Map
import Data.Text (Text, pack)
import Data.Text qualified as T
import Data.Text qualified as Text
import Data.Text.Encoding (decodeUtf8)
import Data.Vector qualified as Vec
import Data.Vector.Storable qualified as SVec
import Data.Version (showVersion)
import Graphics.Vty qualified as V
import System.Console.Haskeline qualified as Readline
import Paths_hevm qualified as Paths
import Text.Wrap
import Witch (into)

data Name
  = AbiPane
  | StackPane
  | BytecodePane
  | TracePane
  | SolidityPane
  | TestPickerPane
  | BrowserPane
  | Pager
  deriving (Eq, Show, Ord)

type UiWidget = Widget Name

data UiVmState = UiVmState
  { vm         :: VM
  , step       :: Int
  , snapshots  :: Map Int (VM, Stepper ())
  , stepper    :: Stepper ()
  , showMemory :: Bool
  , testOpts   :: UnitTestOptions
  }

data UiTestPickerState = UiTestPickerState
  { tests :: List Name (Text, Text)
  , dapp  :: DappInfo
  , opts  :: UnitTestOptions
  }

data UiBrowserState = UiBrowserState
  { contracts :: List Name (Addr, Contract)
  , vm        :: UiVmState
  }

data UiState
  = ViewVm UiVmState
  | ViewContracts UiBrowserState
  | ViewPicker UiTestPickerState
  | ViewHelp UiVmState

makeFieldLabelsNoPrefix ''UiVmState
makeFieldLabelsNoPrefix ''UiTestPickerState
makeFieldLabelsNoPrefix ''UiBrowserState
makePrisms ''UiState

-- caching VM states lets us backstep efficiently
snapshotInterval :: Int
snapshotInterval = 50

type Pred a = a -> Bool

data StepMode
  = Step !Int                  -- ^ Run a specific number of steps
  | StepUntil (Pred VM)        -- ^ Finish when a VM predicate holds

-- | Each step command in the terminal should finish immediately
-- with one of these outcomes.
data Continuation a
     = Stopped a              -- ^ Program finished
     | Continue (Stepper a)   -- ^ Took one step; more steps to go


-- | This turns a @Stepper@ into a state action usable
-- from within the TTY loop, yielding a @StepOutcome@ depending on the @StepMode@.
interpret
  :: (?fetcher :: Fetcher
  ,   ?maxIter :: Maybe Integer)
  => StepMode
  -> Stepper a
  -> StateT UiVmState IO (Continuation a)
interpret mode =

  -- Like the similar interpreters in @EVM.UnitTest@ and @EVM.VMTest@,
  -- this one is implemented as an "operational monad interpreter".

  eval . Operational.view
  where
    eval
      :: Operational.ProgramView Stepper.Action a
      -> StateT UiVmState IO (Continuation a)

    eval (Operational.Return x) =
      pure (Stopped x)

    eval (action Operational.:>>= k) =
      case action of

        Stepper.Run -> do
          -- Have we reached the final result of this action?
          use (#vm % #result) >>= \case
            Just _ -> do
              -- Yes, proceed with the next action.
              vm <- use #vm
              interpret mode (k vm)
            Nothing -> do
              -- No, keep performing the current action
              keepExecuting mode (Stepper.run >>= k)

        -- Stepper wants to keep executing?
        Stepper.Exec -> do
          -- Have we reached the final result of this action?
          use (#vm % #result) >>= \case
            Just r ->
              -- Yes, proceed with the next action.
              interpret mode (k r)
            Nothing -> do
              -- No, keep performing the current action
              keepExecuting mode (Stepper.exec >>= k)

        -- Stepper is waiting for user input from a query
        Stepper.Ask (PleaseChoosePath _ cont) -> do
          -- ensure we aren't stepping past max iterations
          vm <- use #vm
          case maxIterationsReached vm ?maxIter of
            Nothing -> pure $ Continue (k ())
            Just n -> interpret mode (Stepper.evm (cont (not n)) >>= k)

        -- Stepper wants to make a query and wait for the results?
        Stepper.Wait (PleaseAskSMT (Lit c) _ continue) ->
          interpret mode (Stepper.evm (continue (Case (c > 0))) >>= k)
        Stepper.Wait q -> do
          do m <- liftIO (?fetcher q)
             interpret mode (Stepper.evm m >>= k)

        -- Stepper wants to make a query and wait for the results?
        Stepper.IOAct q -> do
          Brick.zoom (toLensVL #vm) (StateT (runStateT q)) >>= interpret mode . k

        -- Stepper wants to modify the VM.
        Stepper.EVM m -> do
          vm <- use #vm
          let (r, vm1) = runState m vm
          assign #vm vm1
          interpret mode (Stepper.exec >> (k r))

keepExecuting :: (?fetcher :: Fetcher
              ,   ?maxIter :: Maybe Integer)
              => StepMode
              -> Stepper a
              -> StateT UiVmState IO (Continuation a)
keepExecuting mode restart = case mode of
  Step 0 -> do
    -- We come here when we've continued while stepping,
    -- either from a query or from a return;
    -- we should pause here and wait for the user.
    pure (Continue restart)

  Step i -> do
    -- Run one instruction and recurse
    stepOneOpcode restart
    interpret (Step (i - 1)) restart

  StepUntil p -> do
    vm <- use #vm
    if p vm
      then
        interpret (Step 0) restart
      else do
        -- Run one instruction and recurse
        stepOneOpcode restart
        interpret (StepUntil p) restart

isUnitTestContract :: Text -> DappInfo -> Bool
isUnitTestContract name dapp =
  elem name (map fst dapp.unitTests)

mkVty :: IO V.Vty
mkVty = do
  vty <- V.mkVty V.defaultConfig
  V.setMode (V.outputIface vty) V.BracketedPaste True
  pure vty

runFromVM :: SolverGroup -> Fetch.RpcInfo -> Maybe Integer -> DappInfo -> VM -> IO VM
runFromVM solvers rpcInfo maxIter' dappinfo vm = do

  let
    opts = UnitTestOptions
      { solvers       = solvers
      , rpcInfo       = rpcInfo
      , verbose       = Nothing
      , maxIter       = maxIter'
      , askSmtIters   = 1
      , smtTimeout    = Nothing
      , smtDebug      = False
      , solver        = Nothing
      , maxDepth      = Nothing
      , match         = ""
      , fuzzRuns      = 1
      , replay        = internalError "irrelevant"
      , vmModifier    = id
      , testParams    = internalError "irrelevant"
      , dapp          = dappinfo
      , ffiAllowed    = False
      , covMatch       = Nothing
      }
    ui0 = initUiVmState vm opts (void Stepper.execFully)

  v <- mkVty
  ui2 <- customMain v mkVty Nothing (app opts) (ViewVm ui0)
  case ui2 of
    ViewVm ui -> pure ui.vm
    _ -> internalError "customMain returned prematurely"


initUiVmState :: VM -> UnitTestOptions -> Stepper () -> UiVmState
initUiVmState vm0 opts script =
  UiVmState
    { vm           = vm0
    , stepper      = script
    , step         = 0
    , snapshots    = singleton 0 (vm0, script)
    , showMemory   = False
    , testOpts     = opts
    }


-- filters out fuzztests, unless they have
-- explicitly been given an argument by `replay`
debuggableTests :: UnitTestOptions -> (Text, [(Test, [AbiType])]) -> [(Text, Text)]
debuggableTests UnitTestOptions{..} (contractname, tests) = case replay of
  Nothing -> [(contractname, extractSig $ fst x) | x <- tests, not $ isFuzzTest x]
  Just (sig, _) -> [(contractname, extractSig $ fst x) | x <- tests, not (isFuzzTest x) || extractSig (fst x) == sig]

isFuzzTest :: (Test, [AbiType]) -> Bool
isFuzzTest (SymbolicTest _, _) = False
isFuzzTest (ConcreteTest _, []) = False
isFuzzTest (ConcreteTest _, _) = True
isFuzzTest (InvariantTest _, _) = True

main :: UnitTestOptions -> FilePath -> Maybe BuildOutput -> IO ()
main opts root buildOutput = do
  let
    dapp = maybe emptyDapp (dappInfo root) buildOutput
    ui = ViewPicker $ UiTestPickerState
      { tests =
          list
            TestPickerPane
            (Vec.fromList
             (concatMap
              (debuggableTests opts)
              dapp.unitTests))
            1
      , dapp = dapp
      , opts = opts
      }
  v <- mkVty
  _ <- customMain v mkVty Nothing (app opts) (ui :: UiState)
  pure ()

takeStep
  :: (?fetcher :: Fetcher
     ,?maxIter :: Maybe Integer)
  => UiVmState
  -> StepMode
  -> EventM n UiState ()
takeStep ui mode =
  liftIO nxt >>= \case
    (Stopped (), _) ->
      pure ()
    (Continue steps, ui') ->
      put (ViewVm (ui' & set #stepper steps))
  where
    m = interpret mode ui.stepper
    nxt = runStateT m ui

backstepUntil
  :: (?fetcher :: Fetcher
     ,?maxIter :: Maybe Integer)
  => (UiVmState -> Pred VM) -> EventM n UiState ()
backstepUntil p = get >>= \case
  ViewVm s ->
    case s.step of
      0 -> pure ()
      n -> do
        s1 <- liftIO $ backstep s
        let
          -- find a previous vm that satisfies the predicate
          snapshots' = Data.Map.filter (p s1 . fst) s1.snapshots
        case lookupLT n snapshots' of
          -- If no such vm exists, go to the beginning
          Nothing ->
            let
              (step', (vm', stepper')) = fromJust $ lookupLT (n - 1) s.snapshots
              s2 = s1
                & set #vm vm'
                & set (#vm % #cache) s1.vm.cache
                & set #step step'
                & set #stepper stepper'
            in takeStep s2 (Step 0)
          -- step until the predicate doesn't hold
          Just (step', (vm', stepper')) ->
            let
              s2 = s1
                & set #vm vm'
                & set (#vm % #cache) s1.vm.cache
                & set #step step'
                & set #stepper stepper'
            in takeStep s2 (StepUntil (not . p s1))
  _ -> pure ()

backstep
  :: (?fetcher :: Fetcher
     ,?maxIter :: Maybe Integer)
  => UiVmState -> IO UiVmState
backstep s =
  case s.step of
    -- We're already at the first step; ignore command.
    0 -> pure s
    -- To step backwards, we revert to the previous snapshot
    -- and execute n - 1 `mod` snapshotInterval steps from there.

    -- We keep the current cache so we don't have to redo
    -- any blocking queries, and also the memory view.
    n ->
      let
        (step, (vm, stepper)) = fromJust $ lookupLT n s.snapshots
        s1 = s
          & set #vm vm
          & set (#vm % #cache) s.vm.cache
          & set #step step
          & set #stepper stepper
        stepsToTake = n - step - 1

      in
        runStateT (interpret (Step stepsToTake) stepper) s1 >>= \case
          (Continue steps, ui') -> pure $ ui' & set #stepper steps
          _ -> internalError "unexpected end"

appEvent
  :: (?fetcher::Fetcher, ?maxIter :: Maybe Integer) =>
  BrickEvent Name e ->
  EventM Name UiState ()

-- Contracts: Down - list down
appEvent (VtyEvent e@(V.EvKey V.KDown [])) = get >>= \case
  ViewContracts _s -> do
    Brick.zoom
      (traverseOf $ _ViewContracts % #contracts)
      (handleListEvent e)
    pure ()
  ViewPicker _s -> do
    Brick.zoom
      (traverseOf $ _ViewPicker % #tests)
      (handleListEvent e)
    pure()
  _ -> pure ()

-- Contracts: Up - list up
-- Page: Up - scroll
appEvent (VtyEvent e@(V.EvKey V.KUp [])) = get >>= \case
  ViewContracts _s -> do
    Brick.zoom
      (traverseOf $ _ViewContracts % #contracts)
      (handleListEvent e)
  ViewPicker _s -> do
    Brick.zoom
      (traverseOf $ _ViewPicker % #tests)
      (handleListEvent e)
    pure()
  _ -> pure ()

-- Vm Overview: Esc - return to test picker or exit
-- Any: Esc - return to Vm Overview or Exit
appEvent (VtyEvent (V.EvKey V.KEsc [])) = get >>= \case
  ViewVm s -> do
    let opts = s ^. #testOpts
        dapp = opts.dapp
        tests = concatMap (debuggableTests opts) dapp.unitTests
    case tests of
      [] -> halt
      ts ->
        put $ ViewPicker $ UiTestPickerState
          { tests = list TestPickerPane (Vec.fromList ts) 1
          , dapp  = dapp
          , opts  = opts
          }
  ViewHelp s -> put (ViewVm s)
  ViewContracts s -> put (ViewVm $ s ^. #vm)
  _ -> halt

-- Vm Overview: Enter - open contracts view
-- UnitTest Picker: Enter - select from list
appEvent (VtyEvent (V.EvKey V.KEnter [])) = get >>= \case
  ViewVm s ->
    put . ViewContracts $ UiBrowserState
      { contracts =
          list
            BrowserPane
            (Vec.fromList (Map.toList s.vm.env.contracts))
            2
      , vm = s
      }
  ViewPicker s ->
    case listSelectedElement s.tests of
      Nothing -> internalError "nothing selected"
      Just (_, x) -> do
        let initVm  = initialUiVmStateForTest s.opts x
        put (ViewVm initVm)
  _ -> pure ()

-- Vm Overview: m - toggle memory pane
appEvent (VtyEvent (V.EvKey (V.KChar 'm') [])) = get >>= \case
  ViewVm s -> put (ViewVm $ over #showMemory not s)
  _ -> pure ()

-- Vm Overview: h - open help view
appEvent (VtyEvent (V.EvKey (V.KChar 'h') [])) = get >>= \case
  ViewVm s -> put (ViewHelp s)
  _ -> pure ()

-- Vm Overview: spacebar - read input
appEvent (VtyEvent (V.EvKey (V.KChar ' ') [])) =
  let
    loop = do
      Readline.getInputLine "% " >>= \case
        Just hey -> Readline.outputStrLn hey
        Nothing  -> pure ()
      Readline.getInputLine "% " >>= \case
        Just hey' -> Readline.outputStrLn hey'
        Nothing   -> pure ()
   in do
    s <- get
    suspendAndResume $ do
      Readline.runInputT Readline.defaultSettings loop
      pure s

-- todo refactor to zipper step forward
-- Vm Overview: n - step
appEvent (VtyEvent (V.EvKey (V.KChar 'n') [])) = get >>= \case
  ViewVm s ->
    when (isNothing (s ^. #vm % #result)) $
      takeStep s (Step 1)
  _ -> pure ()

-- Vm Overview: N - step
appEvent (VtyEvent (V.EvKey (V.KChar 'N') [])) = get >>= \case
  ViewVm s ->
    when (isNothing (s ^. #vm % #result)) $
      takeStep s (StepUntil (isNextSourcePosition s))
  _ -> pure ()

-- Vm Overview: C-n - step
appEvent (VtyEvent (V.EvKey (V.KChar 'n') [V.MCtrl])) = get >>= \case
  ViewVm s ->
    when (isNothing (s ^. #vm % #result)) $
      takeStep s (StepUntil (isNextSourcePositionWithoutEntering s))
  _ -> pure ()

-- Vm Overview: e - step
appEvent (VtyEvent (V.EvKey (V.KChar 'e') [])) = get >>= \case
  ViewVm s ->
    when (isNothing (s ^. #vm % #result)) $
      takeStep s (StepUntil (isExecutionHalted s))
  _ -> pure ()

-- Vm Overview: a - step
appEvent (VtyEvent (V.EvKey (V.KChar 'a') [])) = get >>= \case
  ViewVm s ->
    -- We keep the current cache so we don't have to redo
    -- any blocking queries.
    let
      (vm, stepper) = fromJust (Map.lookup 0 s.snapshots)
      s' = s
        & set #vm vm
        & set (#vm % #cache) s.vm.cache
        & set #step 0
        & set #stepper stepper

    in takeStep s' (Step 0)
  _ -> pure ()

-- Vm Overview: p - backstep
appEvent (VtyEvent (V.EvKey (V.KChar 'p') [])) = get >>= \case
  ViewVm s ->
    case s.step of
      0 ->
        -- We're already at the first step; ignore command.
        pure ()
      n -> do
        -- To step backwards, we revert to the previous snapshot
        -- and execute n - 1 `mod` snapshotInterval steps from there.

        -- We keep the current cache so we don't have to redo
        -- any blocking queries, and also the memory view.
        let
          (step, (vm, stepper)) = fromJust $ lookupLT n s.snapshots
          s1 = s
            & set #vm vm -- set the vm to the one from the snapshot
            & set (#vm % #cache) s.vm.cache -- persist the cache
            & set #step step
            & set #stepper stepper
          stepsToTake = n - step - 1

        takeStep s1 (Step stepsToTake)
  _ -> pure ()

-- Vm Overview: P - backstep to previous source
appEvent (VtyEvent (V.EvKey (V.KChar 'P') [])) =
  backstepUntil isNextSourcePosition

-- Vm Overview: c-p - backstep to previous source avoiding CALL and CREATE
appEvent (VtyEvent (V.EvKey (V.KChar 'p') [V.MCtrl])) =
  backstepUntil isNextSourcePositionWithoutEntering

-- Vm Overview: 0 - choose no jump
appEvent (VtyEvent (V.EvKey (V.KChar '0') [])) = get >>= \case
  ViewVm s ->
    case view (#vm % #result) s of
      Just (HandleEffect (Choose (PleaseChoosePath _ contin))) ->
        takeStep (s & set #stepper (Stepper.evm (contin True) >> s.stepper))
          (Step 1)
      _ -> pure ()
  _ -> pure ()

-- Vm Overview: 1 - choose jump
appEvent (VtyEvent (V.EvKey (V.KChar '1') [])) = get >>= \case
  ViewVm s ->
    case s.vm.result of
      Just (HandleEffect (Choose (PleaseChoosePath _ contin))) ->
        takeStep (s & set #stepper (Stepper.evm (contin False) >> s.stepper))
          (Step 1)
      _ -> pure ()
  _ -> pure ()

-- Page: C-f - Page down
appEvent (VtyEvent (V.EvKey (V.KChar 'f') [V.MCtrl])) =
  vScrollPage (viewportScroll TracePane) Down

-- Page: C-b - Page up
appEvent (VtyEvent (V.EvKey (V.KChar 'b') [V.MCtrl])) =
  vScrollPage (viewportScroll TracePane) Up

-- UnitTest Picker: (main) - render list
appEvent (VtyEvent e) = do
  Brick.zoom (traverseOf (_ViewPicker % #tests))
    (handleListEvent e)

-- Default
appEvent _ = pure ()

app :: UnitTestOptions -> App UiState () Name
app UnitTestOptions{..} =
  let ?fetcher = Fetch.oracle solvers rpcInfo
      ?maxIter = maxIter
  in App
  { appDraw = drawUi
  , appChooseCursor = neverShowCursor
  , appHandleEvent = appEvent
  , appStartEvent = pure ()
  , appAttrMap = const (attrMap V.defAttr myTheme)
  }

initialUiVmStateForTest
  :: UnitTestOptions
  -> (Text, Text)
  -> UiVmState
initialUiVmStateForTest opts@UnitTestOptions{..} (theContractName, theTestName) = initUiVmState vm0 opts script
  where
    cd = case test of
      SymbolicTest _ -> symCalldata theTestName types [] (AbstractBuf "txdata")
      _ -> (internalError "unreachable", error $ internalError "unreachable")
    (test, types) = fromJust $ find (\(test',_) -> extractSig test' == theTestName) $ unitTestMethods testContract
    testContract = fromJust $ Map.lookup theContractName dapp.solcByName
    vm0 =
      initialUnitTestVm opts testContract
    script = do
      Stepper.evm . pushTrace . EntryTrace $
        "test " <> theTestName <> " (" <> theContractName <> ")"
      initializeUnitTest opts testContract
      case test of
        ConcreteTest _ -> do
          let args = case replay of
                       Nothing -> emptyAbi
                       Just (sig, callData) ->
                         if theTestName == sig
                         then decodeAbiValue (AbiTupleType (Vec.fromList types)) callData
                         else emptyAbi
          void (runUnitTest opts theTestName args)
        SymbolicTest _ -> do
          void (execSymTest opts theTestName cd)
        InvariantTest _ -> do
          targets <- getTargetContracts opts
          let randomRun = initialExplorationStepper opts theTestName [] targets (fromMaybe 20 maxDepth)
          void $ case replay of
            Nothing -> randomRun
            Just (sig, cd') ->
              if theTestName == sig
              then initialExplorationStepper opts theTestName (decodeCalls cd') targets (length (decodeCalls cd'))
              else randomRun

myTheme :: [(AttrName, V.Attr)]
myTheme =
  [ (selectedAttr, V.defAttr `V.withStyle` V.standout)
  , (dimAttr, V.defAttr `V.withStyle` V.dim)
  , (borderAttr, V.defAttr `V.withStyle` V.dim)
  , (wordAttr, fg V.yellow)
  , (boldAttr, V.defAttr `V.withStyle` V.bold)
  , (activeAttr, V.defAttr `V.withStyle` V.standout)
  ]

drawUi :: UiState -> [UiWidget]
drawUi (ViewVm s) = drawVm s
drawUi (ViewPicker s) = drawTestPicker s
drawUi (ViewContracts s) = drawVmBrowser s
drawUi (ViewHelp _) = drawHelpView

drawHelpView :: [UiWidget]
drawHelpView =
    [ center . borderWithLabel version .
      padLeftRight 4 . padTopBottom 2 .  str $
        "Esc    Exit the debugger\n\n" <>
        "a      Step to start\n" <>
        "e      Step to end\n" <>
        "n      Step fwds by one instruction\n" <>
        "N      Step fwds to the next source position\n" <>
        "C-n    Step fwds to the next source position skipping CALL & CREATE\n" <>
        "p      Step back by one instruction\n\n" <>
        "P      Step back to the previous source position\n\n" <>
        "C-p    Step back to the previous source position skipping CALL & CREATE\n\n" <>
        "m      Toggle memory pane\n" <>
        "0      Choose the branch which does not jump \n" <>
        "1      Choose the branch which does jump \n" <>
        "Down   Step to next entry in the callstack / Scroll memory pane\n" <>
        "Up     Step to previous entry in the callstack / Scroll memory pane\n" <>
        "C-f    Page memory pane fwds\n" <>
        "C-b    Page memory pane back\n\n" <>
        "Enter  Contracts browser"
    ]
    where
      version =
        txt "Hevm " <+>
        str (showVersion Paths.version) <+>
        txt " - Key bindings"

drawTestPicker :: UiTestPickerState -> [UiWidget]
drawTestPicker ui =
  [ center . borderWithLabel (txt "Unit tests") .
      hLimit 80 $
        renderList
          (\selected (x, y) ->
             withHighlight selected $
               txt " Debug " <+> txt (contractNamePart x) <+> txt "::" <+> txt y)
          True
          ui.tests
  ]

drawVmBrowser :: UiBrowserState -> [UiWidget]
drawVmBrowser ui =
  [ hBox
      [ borderWithLabel (txt "Contracts") .
          hLimit 60 $
            renderList
              (\selected (k, c') ->
                 withHighlight selected . txt . mconcat $
                   [ fromMaybe "<unknown contract>" $
                       Map.lookup (maybeHash c') dapp.solcByHash <&> (.contractName) . snd
                   , "\n"
                   , "  ", pack (show k)
                   ])
              True
              ui.contracts
      , case snd <$> Map.lookup (maybeHash c) dapp.solcByHash of
          Nothing ->
            hBox
              [ borderWithLabel (txt "Contract information") . padBottom Max . padRight Max $ vBox
                  [ txt ("Codehash: " <> pack (show c.codehash))
                  , txt ("Nonce: "    <> showWordExact c.nonce)
                  , txt ("Balance: "  <> showWordExact c.balance)
                  --, txt ("Storage: "  <> storageDisplay (view storage c)) -- TODO: fix this
                  ]
                ]
          Just sol ->
            hBox
              [ borderWithLabel (txt "Contract information") . padBottom Max . padRight (Pad 2) $ vBox
                  [ txt "Name: " <+> txt (contractNamePart sol.contractName)
                  , txt "File: " <+> txt (contractPathPart sol.contractName)
                  , txt " "
                  , txt "Constructor inputs:"
                  , vBox . flip map sol.constructorInputs $
                      \(name, abiType) -> txt ("  " <> name <> ": " <> abiTypeSolidity abiType)
                  , txt "Public methods:"
                  , vBox . flip map (sort (Map.elems sol.abiMap)) $
                      \method -> txt ("  " <> method.methodSignature)
                  --, txt ("Storage:" <> storageDisplay (view storage c)) -- TODO: fix this
                  ]
              , borderWithLabel (txt "Storage slots") . padBottom Max . padRight Max $ vBox
                  (map txt (storageLayout dapp sol))
              ]
      ]
  ]
  where
    dapp = ui.vm.testOpts.dapp
    (_, (_, c)) = fromJust $ listSelectedElement ui.contracts
--        currentContract  = view (dappSolcByHash . ix ) dapp
    maybeHash ch = fromJust (internalError "cannot find concrete codehash for partially symbolic code") (maybeLitWord ch.codehash)

drawVm :: UiVmState -> [UiWidget]
drawVm ui =
  -- EVM debugging needs a lot of space because of the 256-bit words
  -- in both the bytecode and the stack .
  --
  -- If on a very tall display, prefer a vertical layout.
  --
  -- Actually the horizontal layout would be preferrable if the display
  -- is both very tall and very wide, but this is okay for now.
  [ ifTallEnough (20 * 4)
      ( vBox
        [ vLimit 20 $ drawBytecodePane ui
        , vLimit 20 $ drawStackPane ui
        , drawSolidityPane ui
        , vLimit 20 $ drawTracePane ui
        , vLimit 2 drawHelpBar
        ]
      )
      ( vBox
        [ hBox
          [ vLimit 20 $ drawBytecodePane ui
          , vLimit 20 $ drawStackPane ui
          ]
        , hBox
          [ drawSolidityPane ui
          , drawTracePane ui
          ]
        , vLimit 2 drawHelpBar
        ]
      )
  ]

drawHelpBar :: UiWidget
drawHelpBar = hBorder <=> hCenter help
  where
    help =
      hBox (map (\(k, v) -> txt k <+> dim (txt (" (" <> v <> ")  "))) helps)

    helps =
      [
        ("n", "step")
      , ("p", "step back")
      , ("a", "step to start")
      , ("e", "step to end")
      , ("m", "toggle memory")
      , ("Esc", "exit")
      , ("h", "more help")
      ]

stepOneOpcode :: Stepper a -> StateT UiVmState IO ()
stepOneOpcode restart = do
  n <- use #step
  when (n > 0 && n `mod` snapshotInterval == 0) $ do
    vm <- use #vm
    modifying #snapshots (insert n (vm, void restart))
  modifying #vm (execState exec1)
  modifying #step (+ 1)

isNewTraceAdded
  :: UiVmState -> Pred VM
isNewTraceAdded ui vm =
  let
    currentTraceTree = length <$> traceForest ui.vm
    newTraceTree = length <$> traceForest vm
  in currentTraceTree /= newTraceTree

isNextSourcePosition
  :: UiVmState -> Pred VM
isNextSourcePosition ui vm =
  let dapp = ui.testOpts.dapp
      initialPosition = currentSrcMap dapp ui.vm
  in currentSrcMap dapp vm /= initialPosition

isNextSourcePositionWithoutEntering
  :: UiVmState -> Pred VM
isNextSourcePositionWithoutEntering ui vm =
  let
    dapp            = ui.testOpts.dapp
    vm0             = ui.vm
    initialPosition = currentSrcMap dapp vm0
    initialHeight   = length vm0.frames
  in
    case currentSrcMap dapp vm of
      Nothing ->
        False
      Just here ->
        let
          moved = Just here /= initialPosition
          deeper = length vm.frames > initialHeight
          boring =
            case srcMapCode dapp.sources here of
              Just bs ->
                BS.isPrefixOf "contract " bs
              Nothing ->
                True
        in
           moved && not deeper && not boring

isExecutionHalted :: UiVmState -> Pred VM
isExecutionHalted _ vm = isJust vm.result

currentSrcMap :: DappInfo -> VM -> Maybe SrcMap
currentSrcMap dapp vm = do
  this <- currentContract vm
  i <- this.opIxMap SVec.!? vm.state.pc
  srcMap dapp this i

drawStackPane :: UiVmState -> UiWidget
drawStackPane ui =
  let
    gasText = showWordExact (into ui.vm.state.gas)
    labelText = txt ("Gas available: " <> gasText <> "; stack:")
    stackList = list StackPane (Vec.fromList $ zip [(1 :: Int)..] (simplify <$> ui.vm.state.stack)) 2
  in hBorderWithLabel labelText <=>
    renderList
      (\_ (i, w) ->
         vBox
           [ withHighlight True (str ("#" ++ show i ++ " "))
               <+> ourWrap (Text.unpack $ prettyIfConcreteWord w)
           , dim (txt ("   " <> case maybeLitWord w of
                       Nothing -> ""
                       Just u -> showWordExplanation u ui.testOpts.dapp))
           ])
      False
      stackList

message :: VM -> String
message vm =
  case vm.result of
    Just (VMSuccess (ConcreteBuf msg)) ->
      "VMSuccess: " <> (show $ ByteStringS msg)
    Just (VMSuccess (msg)) ->
      "VMSuccess: <symbolicbuffer> " <> (show msg)
    Just (VMFailure (Revert msg)) ->
      "VMFailure: " <> (show msg)
    Just (VMFailure err) ->
      "VMFailure: " <> show err
    Just (Unfinished p) ->
      "Could not continue execution: " <> show p
    Just (HandleEffect e) ->
      "Handling side effect: " <> show e
    Nothing ->
      "Executing EVM code in " <> show vm.state.contract


drawBytecodePane :: UiVmState -> UiWidget
drawBytecodePane ui =
  let
    vm = ui.vm
    move = maybe id listMoveTo $ vmOpIx vm
  in
    hBorderWithLabel (str $ message vm) <=>
    Centered.renderList
      (\active x -> if not active
                    then withDefAttr dimAttr (opWidget x)
                    else withDefAttr boldAttr (opWidget x))
      False
      (move $ list BytecodePane
        (maybe mempty (.codeOps) (currentContract vm))
        1)


dim :: Widget n -> Widget n
dim = withDefAttr dimAttr

withHighlight :: Bool -> Widget n -> Widget n
withHighlight False = withDefAttr dimAttr
withHighlight True  = withDefAttr boldAttr

prettyIfConcrete :: Expr Buf -> String
prettyIfConcrete (ConcreteBuf x) = prettyHex 40 x
prettyIfConcrete x = T.unpack $ formatExpr $ simplify x

drawTracePane :: UiVmState -> UiWidget
drawTracePane s =
  let vm = s.vm
      dapp = s.testOpts.dapp
      traceList =
        list
          TracePane
          (Vec.fromList
            . Text.lines
            . showTraceTree dapp
            $ vm)
          1

  in case s.showMemory of
    True -> viewport TracePane Vertical $
        hBorderWithLabel (txt "Calldata")
        <=> ourWrap (prettyIfConcrete vm.state.calldata)
        <=> hBorderWithLabel (txt "Returndata")
        <=> ourWrap (prettyIfConcrete vm.state.returndata)
        <=> hBorderWithLabel (txt "Output")
        <=> ourWrap (maybe "" show vm.result)
        <=> hBorderWithLabel (txt "Cache")
        <=> ourWrap (show vm.cache.path)
        <=> hBorderWithLabel (txt "Path Conditions")
        <=> (ourWrap $ show $ vm.constraints)
        <=> hBorderWithLabel (txt "Memory")
        <=> (ourWrap (prettyIfConcrete vm.state.memory))
    False ->
      hBorderWithLabel (txt "Trace")
      <=> renderList
            (\_ x -> txt x)
            False
            (listMoveTo (length traceList) traceList)

ourWrap :: String -> Widget n
ourWrap = strWrapWith settings
  where
    settings = WrapSettings
      { preserveIndentation = True
      , breakLongWords = True
      , fillStrategy = NoFill
      , fillScope = FillAfterFirst
      }

solidityList :: VM -> DappInfo -> List Name (Int, ByteString)
solidityList vm dapp =
  list SolidityPane
    (case currentSrcMap dapp vm of
        Nothing -> mempty
        Just x ->
          fromMaybe
            (internalError "unable to find line for source map")
            (preview (
              ix x.file
              % to (Vec.imap (,)))
            dapp.sources.lines))
    1

drawSolidityPane :: UiVmState -> UiWidget
drawSolidityPane ui =
  let dapp = ui.testOpts.dapp
      dappSrcs = dapp.sources
      vm = ui.vm
  in case currentSrcMap dapp vm of
    Nothing -> padBottom Max (hBorderWithLabel (txt "<no source map>"))
    Just sm ->
          let
            rows = dappSrcs.lines !? sm.file
            subrange :: Int -> Maybe (Int, Int)
            subrange i = do
              rs <- rows
              lineSubrange rs (sm.offset, sm.length) i
            fileName :: Maybe Text
            fileName = T.pack . fst <$> (dapp.sources.files !? sm.file)
            lineNo :: Maybe Int
            lineNo = ((\a -> Just (a - 1)) . snd) =<< srcMapCodePos dapp.sources sm
          in vBox
            [ hBorderWithLabel $
                txt (fromMaybe "<unknown>" fileName)
                  <+> str (":" ++ (maybe "?" show lineNo))

                  -- Show the AST node type if present
                  <+> txt (" (" <> fromMaybe "?"
                                    (dapp.astSrcMap sm
                                       >>= preview (key "name" % _String)) <> ")")
            , Centered.renderList
                (\_ (i, line) ->
                   let s = case decodeUtf8 line of "" -> " "; y -> y
                   in case subrange i of
                        Nothing -> withHighlight False (txt s)
                        Just (a, b) ->
                          let (x, y, z) = ( Text.take a s
                                          , Text.take b (Text.drop a s)
                                          , Text.drop (a + b) s
                                          )
                          in hBox [ withHighlight False (txt x)
                                  , withHighlight True (txt y)
                                  , withHighlight False (txt z)
                                  ])
                False
                ((maybe id listMoveTo lineNo)
                  (solidityList vm dapp))
            ]

ifTallEnough :: Int -> Widget n -> Widget n -> Widget n
ifTallEnough need w1 w2 =
  Widget Greedy Greedy $ do
    c <- getContext
    if view (lensVL availHeightL) c > need
      then render w1
      else render w2

opWidget :: (Integral a, Show a) => (a, Op) -> Widget n
opWidget = txt . pack . opString

selectedAttr :: AttrName; selectedAttr = attrName "selected"
dimAttr :: AttrName; dimAttr = attrName "dim"
wordAttr :: AttrName; wordAttr = attrName "word"
boldAttr :: AttrName; boldAttr = attrName "bold"
activeAttr :: AttrName; activeAttr = attrName "active"