gigaparsec-0.2.2.0: src/Text/Gigaparsec/Debug.hs
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE ExistentialQuantification, NamedFieldPuns #-}
{-|
Module : Text.Gigaparsec.Debug
Description : This module contains the very useful debugging combinator, as well as breakpoints.
License : BSD-3-Clause
Maintainer : Jamie Willis, Gigaparsec Maintainers
Stability : stable
This module contains the very useful debugging combinators 'debug' and 'debugWith', as well as
breakpoints that can be used to pause parsing execution.
@since 0.2.1.0
-}
module Text.Gigaparsec.Debug (debug, debugWith, debugConfig, DebugConfig(..), WatchedReg(..), Break(..)) where
import Text.Gigaparsec.Internal (Parsec)
import Text.Gigaparsec.Internal.RT (Reg, readReg)
import Text.Gigaparsec.Internal qualified as Internal
import Text.Gigaparsec.Internal.RT qualified as Internal
import Control.Monad (when, forM)
import System.IO (hGetEcho, hSetEcho, hPutStr, stdin, stdout, Handle)
import Data.List (intercalate, isPrefixOf)
import Data.List.NonEmpty (NonEmpty((:|)), (<|))
import Data.List.NonEmpty qualified as NonEmpty (toList)
import System.Console.Pretty (color, supportsPretty, Color(Green, White, Red, Blue))
{-|
Configuration that allows for customising the behaviour of a 'debugWith'
combinator.
-}
type DebugConfig :: *
data DebugConfig = DebugConfig {
ascii :: !Bool, -- ^ should the output of the combinator be in plain uncoloured ascii?
breakPoint :: !Break, -- ^ should parsing execution be paused when entering or leaving this combinator?
watchedRegs :: ![WatchedReg], -- ^ what registers should have their values tracked during debugging?
handle :: !Handle -- ^ where should the output of the combinator be sent?
}
{-|
The plain configuration used by the 'debug' combinator itself. It will have coloured
terminal output (if available), never pause the parsing execution, not track any registers,
and print its output to 'stdout'.
-}
debugConfig :: DebugConfig
debugConfig = DebugConfig { ascii = False, breakPoint = Never, watchedRegs = [], handle = stdout }
{-|
This type allows for a specified register to be watched by a debug combinator. The
contents of the register must be 'Show'able, and it should be given a name to identify
it within the print-out. Registers containing different types can be simultaneously
tracked, which is why this datatype is existential.
-}
type WatchedReg :: *
data WatchedReg = forall r a. Show a => WatchedReg String -- ^ the name of the register
(Reg r a) -- ^ the register itself
{-|
This is used by 'DebugConfig' to specify whether the parsing should be paused
when passing through a 'debugWith' combinator.
-}
type Break :: *
data Break = OnEntry -- ^ pause the parsing just after entering a debug combinator
| OnExit -- ^ pause the parsing just after leaving a debug combinator
| Always -- ^ pause the parsing both just after entry and exit of a debug combinator
| Never -- ^ do not pause execution when passing through (__default__)
{-|
This combinator allows this parser to be debugged by providing a trace through the execution.
When this combinator is entered, it will print the name assigned to the parser to the console,
as well as the current input context for a few characters that follow.
This parser is then executed. If it succeeded, this combinator again reports the
name along with \"@Good@\" and the input context. If it failed, it reports the name
along with \"@Bad@\" and the input context.
-}
debug :: String -> Parsec a -> Parsec a
debug = debugWith debugConfig
{-|
This combinator allows this parser to be debugged by providing a trace through the execution.
An additional 'DebugConfig' is provided to customise behaviour.
When this combinator is entered, it will print the name assigned to the parser to the
configured handle, as well as the current input context for a few characters that follow.
This parser is then executed. If it succeeded, this combinator again reports the
name along with \"@Good@\" and the input context. If it failed, it reports the name
along with \"@Bad@\" and the input context.
When breakpoints are enabled within the config, the execution of the combinator will pause
on either entry, exit, or both. The parse is resumed by entering any character on standard input.
-}
debugWith :: DebugConfig -> String -> Parsec a -> Parsec a
debugWith config@DebugConfig{ascii} name (Internal.Parsec p) = Internal.Parsec $ \st good bad -> do
-- TODO: could make it so the postamble can print input information from the entry?
ascii' <- (\colourful -> ascii || not colourful) <$> Internal.unsafeIOToRT supportsPretty
let config' = config { ascii = ascii' }
doDebug name Enter st "" config'
let good' x st' = do
let st'' = st' { Internal.debugLevel = Internal.debugLevel st' - 1}
doDebug name Exit st'' (green ascii' " Good") config'
good x st''
let bad' err st' = do
let st'' = st' { Internal.debugLevel = Internal.debugLevel st' - 1}
doDebug name Exit st'' (red ascii' " Bad") config'
bad err st''
p (st { Internal.debugLevel = Internal.debugLevel st + 1}) good' bad'
---------------------------------------------
---- INTERNALS
type Direction :: *
data Direction = Enter | Exit
breakOnEntry :: Break -> Bool
breakOnEntry OnEntry = True
breakOnEntry Always = True
breakOnEntry _ = False
breakOnExit :: Break -> Bool
breakOnExit OnExit = True
breakOnExit Always = True
breakOnExit _ = False
shouldBreak :: Direction -> Break -> Bool
shouldBreak Enter = breakOnEntry
shouldBreak Exit = breakOnExit
doDebug :: String -> Direction -> Internal.State -> String -> DebugConfig -> Internal.RT ()
doDebug name dir st end DebugConfig{..} = do
printInfo handle name dir st end ascii watchedRegs
when (shouldBreak dir breakPoint) waitForUser
printInfo :: Handle -> String -> Direction -> Internal.State -> String -> Bool -> [WatchedReg] -> Internal.RT ()
printInfo handle name dir st@Internal.State{input, line, col} end ascii regs = do
let cs = replace "\n" (newline ascii)
. replace " " (space ascii)
. replace "\r" (carriageReturn ascii)
. replace "\t" (tab ascii)
$ take (5 + 1) input
let cs' = if length cs < (5 + 1) then cs ++ endOfInput ascii else cs
let prelude = portal dir name ++ " " ++ show (line, col) ++ ": "
let caret = replicate (length prelude) ' ' ++ blue ascii "^"
regSummary <-
if null regs then return []
else (++ [""]) . ("watched registers:" :) <$>
forM regs (\(WatchedReg rname reg) ->
(\x -> " " ++ rname ++ " = " ++ show x) <$> readReg reg)
Internal.unsafeIOToRT $
hPutStr handle $ indentAndUnlines st ((prelude ++ cs' ++ end) : caret : regSummary)
waitForUser :: Internal.RT ()
waitForUser = Internal.unsafeIOToRT $ do
echo <- hGetEcho stdin
hSetEcho stdin False
putStrLn "..."
_ <- getChar
hSetEcho stdin echo
render :: Direction -> String
render Enter = ">"
render Exit = "<"
portal :: Direction -> String -> String
portal dir name = render dir ++ name ++ render dir
indent :: Internal.State -> String
indent st = replicate (Internal.debugLevel st * 2) ' '
indentAndUnlines :: Internal.State -> [String] -> String
indentAndUnlines st = unlines . map (indent st ++)
green, red, white, blue :: Bool -> String -> String
green = colour Green
red = colour Red
white = colour White
blue = colour Blue
colour :: Color -> Bool -> String -> String
colour _ True s = s
colour c False s = color c s
newline, space, carriageReturn, tab, endOfInput :: Bool -> String
newline ascii = green ascii "↙"
space ascii = white ascii "·"
carriageReturn ascii = green ascii "←"
tab ascii = white ascii "→"
endOfInput ascii = red ascii "•"
replace :: String -> String -> String -> String
replace needle replacement haystack =
intercalate replacement (NonEmpty.toList (splitOn needle haystack))
splitOn :: String -> String -> NonEmpty String
splitOn pat = go
where go src
| isPrefixOf pat src = "" <| go (drop n src)
| c:cs <- src = let (w :| ws) = go cs in (c : w) :| ws
| otherwise = "" :| []
n = length pat