From 9474d808dcafa1616d7de066311442268d972e7a Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Wed, 21 May 2025 23:26:01 -1000 Subject: [PATCH] ;imp:lib:Hledger.Utils.Debug: more cleanups, docs This and/or the previous related change also changed a few cases which always traced or always logged; now all debug output is consistently either traced or logged. --- hledger-lib/Hledger/Utils/Debug.hs | 329 ++++++++++++++++------------- 1 file changed, 185 insertions(+), 144 deletions(-) diff --git a/hledger-lib/Hledger/Utils/Debug.hs b/hledger-lib/Hledger/Utils/Debug.hs index eb64f4630..2559d3a51 100644 --- a/hledger-lib/Hledger/Utils/Debug.hs +++ b/hledger-lib/Hledger/Utils/Debug.hs @@ -1,30 +1,72 @@ {- | -Here are fancier helpers built on Debug.Trace, with these features: +Here are debug tracing/logging helpers built on Debug.Trace, extracted from the hledger project. +Features: -- short, memorable, greppable function names -- pretty-printing of haskell values, using pretty-simple -- optional ANSI colour -- enabling/disabling debug output with --debug -- debug output levels from 1 to 9, selected by --debug N option -- --debug detected with unsafePerformIO for easy use in pure/IO/startup code -- debug output can be logged instead (for TUI apps) +- they can be built in to your program permanently, and activated by a --debug [LEVEL] option +- they can optionally log to a file instead of stderr (for TUI apps) +- they can be used in IO, pure, or startup code +- values are printed with a label, and pretty-printed (using pretty-simple) +- ANSI colour is used when appropriate. -The "dbg*" functions can be inserted temporarily at points of interest in your code -while debugging; or embedded there permanently, to be activated by --debug [N] on the command line. -They are intended to be relatively easy to remember, search for, and use. +Insert these @dbg*@ helpers at points of interest in your code, +either temporarily while debugging, +or permanently in production code, and activate them with @--debug [1-9]@ on the command line +(@--debug@ with no value means level 1). +For example, this expression: -Parsing the command line, detecting the program name, and logging is done with unsafePerformIO, -allowing these helpers to be used anywhere, eg before command line parsing or in pure code. -If you are working in GHCI and want to change the debug level, you'll need to reload this module. +> dbg4 "foo" foo + +will pretty-print foo with a "foo:" label when it is evaluated, but only if --debug's value is 4 or greater. +In other words: use dbg1 for the most useful debug output, dbg9 for the most specialised/verbose. + +They are intended to be easy to use and to find in your code, with a consistent naming scheme: + +> dbgMsg STR VAL -- trace/log a string in pure code +> dbgMsgIO STR -- trace/log a string in IO +> +> dbg STR VAL -- trace/log a showable value in pure code +> dbgIO STR VAL -- trace/log a showable value in IO +> +> dbgWith SHOWFN VAL -- trace/log any value + +Or if you prefer you can ignore the numbered variants and write an extra argument: + +> dbgMsg LEVEL STR VAL +> dbgMsgIO LEVEL STR +> +> dbg LEVEL STR VAL +> dbgIO LEVEL STR VAL +> +> dbgWith LEVEL SHOWFN VAL + +Haskell values will be pretty-printed by default, using pretty-simple. + +ANSI color will also be used if appropriate, +respecting output capabilities, @NO_COLOR@, and/or a @--color [YNA]@ (or @--colour@) command line option. + +These helpers normally print output on stderr, but can automatically log to a file instead, +which can be useful for TUI apps which are redrawing the screen. +To enable this logging mode, use @withProgName@ to add a ".log" suffix to the program name: + +> main = withProgName "PROGRAM.log" $ do ... + +Now all dbg calls will log to @PROGRAM.log@ in the current directory. + +Logging, and reading the command line\/program name\/output context use unsafePerformIO, +so that these can be used anywhere, including early in your program before command line parsing is complete. +As a consequence, if you are testing in GHCI and want to change the debug level, you'll need to reload this module. + +The @dbg@ function name clashes with the one in Text.Megaparsec.Debug, unfortunately; sorry about that. +If you are also using that, use qualified imports, or our @dbg_@ alias, to avoid the clash. The meaning of debug levels is up to you. Eg hledger uses them as follows: @ Debug level: What to show: ------------ --------------------------------------------------------- -0 normal command output only (no warnings, eg) -1 useful warnings, most common troubleshooting info (config file args, valuation..) +0 normal program output only +1 useful warnings, most common troubleshooting info 2 common troubleshooting info, more detail 3 report options selection 4 report generation @@ -49,54 +91,47 @@ It's not yet possible to select debug output by topic; that would be useful. -- http://hackage.haskell.org/packages/archive/traced/2009.7.20/doc/html/Debug-Traced.html -- https://hackage.haskell.org/package/debug +-- internal helpers, currently not exported: + +-- * Tracing to stderr +-- These print to stderr. +-- This output will be interleaved with the program's normal output, +-- which can be helpful for understanding code execution. +-- +-- ,traceWith +-- ,traceAt +-- ,traceAtWith +-- ,ptrace +-- ,ptraceAt +-- ,ptraceAtIO + +-- * Logging to a log file +-- These append to a PROGRAM.log file in the current directory. +-- PROGRAM is normally the name of the executable, but it can change +-- eg when running in GHCI. So when using these, you should call +-- @withProgName@ to ensure a stable program name. +-- Eg: @main = withProgName "PROGRAM" $ do ...@. +-- +-- ,log' +-- ,logAt +-- ,logIO +-- ,logAtIO +-- ,logWith +-- ,logAtWith +-- ,plogAt +-- ,plogAtIO + {-# LANGUAGE CPP #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} module Hledger.Utils.Debug ( - -- * The program's debug level, from 0 (least debug output) to 9 (most). - -- This is parsed from a command line --debug N option, or --debug meaning 1. + -- * Debug level + -- | This is parsed from a command line --debug N option, or --debug meaning 1. -- The command line is read (once) by unsafePerformIO, allowing this to be used -- easily anywhere in your program. debugLevel - -- * Tracing to stderr - -- These print to stderr. - -- This output will be interleaved with the program's normal output, - -- which can be helpful for understanding code execution. - -- - -- ,traceWith - -- ,traceAt - -- ,traceAtWith - -- ,ptrace - -- ,ptraceAt - -- ,ptraceAtIO - - -- * Logging to a log file - -- These append to a PROGRAM.log file in the current directory. - -- PROGRAM is normally the name of the executable, but it can change - -- eg when running in GHCI. So when using these, you should call - -- @withProgName@ to ensure a stable program name. - -- Eg: @main = withProgName "PROGRAM" $ do ...@. - -- - -- ,log' - -- ,logAt - -- ,logIO - -- ,logAtIO - -- ,logWith - -- ,logAtWith - -- ,plogAt - -- ,plogAtIO - - -- All @dbg*@ functions normally trace to stderr, - -- but they will log to PROGRAM.log instead if the (internal) program name ends with ".log". - -- Eg: @main = withProgName "PROGRAM.log" $ do ...@. - -- This is intended for TUI programs where stderr output is hard to see. - -- - -- They have an effect only when the program's debug level is at or above the - -- level specified by an argument or by the function name. - -- The many variants follow a consistent pattern and aim to reduce typing and cognitive load. - -- * Trace/log a string ,dbgMsg ,dbg0Msg @@ -110,7 +145,7 @@ module Hledger.Utils.Debug ( ,dbg8Msg ,dbg9Msg - -- * In IO + -- * Trace/log a string in IO ,dbgMsgIO ,dbg0MsgIO ,dbg1MsgIO @@ -123,10 +158,9 @@ module Hledger.Utils.Debug ( ,dbg8MsgIO ,dbg9MsgIO - -- * Trace/log a showable value, pretty-printed - -- @dbg@ here clashes with Text.Megaparsec.Debug (dbg), so that module or this one - -- should be imported qualified if you are using both. + -- * Trace/log a value ,dbg + ,dbg_ ,dbg0 ,dbg1 ,dbg2 @@ -138,7 +172,7 @@ module Hledger.Utils.Debug ( ,dbg8 ,dbg9 - -- * In IO + -- * Trace/log a value in IO ,dbgIO ,dbg0IO ,dbg1IO @@ -151,7 +185,7 @@ module Hledger.Utils.Debug ( ,dbg8IO ,dbg9IO - -- * With a custom show function + -- * Trace/log a value with a show function ,dbgWith ,dbg0With ,dbg1With @@ -164,18 +198,18 @@ module Hledger.Utils.Debug ( ,dbg8With ,dbg9With - -- * Utilities, ghc-debug + -- * Utilities + ,lbl_ + ,progName + + -- * ghc-debug helpers ,ghcDebugSupportedInLib ,GhcDebugMode(..) ,ghcDebugMode ,withGhcDebug' ,ghcDebugPause' - -- * Utilities, other - ,lbl_ - ,progName - - -- * Re-exports + -- * Re-exports: Debug.Trace -- ,module Debug.Breakpoint ,module Debug.Trace @@ -197,6 +231,7 @@ import System.IO.Unsafe (unsafePerformIO) import Hledger.Utils.IO (progArgs, pshow, pshow') + -- | The program name as returned by @getProgName@. -- It's best to set this explicitly at program startup with @withProgName@, -- otherwise when running in GHCI (eg) it will change to "". @@ -226,69 +261,6 @@ debugLevel = case dropWhile (/="--debug") progArgs of ['-':'-':'d':'e':'b':'u':'g':'=':v] -> readDef 1 v _ -> 0 --- | Whether ghc-debug support is included in this build, and if so, how it will behave. --- When hledger is built with the @ghcdebug@ cabal flag (off by default, because of extra deps), --- it can listen (on unix ?) for connections from ghc-debug clients like ghc-debug-brick, --- for pausing/resuming the program and inspecting memory usage and profile information. --- --- With a ghc-debug-supporting build, ghc-debug can be enabled by running hledger with --- a negative --debug level. There are three different modes: --- --debug=-1 - run normally (can be paused/resumed by a ghc-debug client), --- --debug=-2 - pause and await client commands at program start (not useful currently), --- --debug=-3 - pause and await client commands at program end. -data GhcDebugMode = - GDNotSupported - | GDDisabled - | GDNoPause - | GDPauseAtStart - | GDPauseAtEnd - -- keep synced with ghcDebugMode - deriving (Eq,Ord,Show) - --- | Is the hledger-lib package built with ghc-debug support ? -ghcDebugSupportedInLib :: Bool -ghcDebugSupportedInLib = -#ifdef GHCDEBUG - True -#else - False -#endif - --- | Should the program open a socket allowing control by ghc-debug-brick or similar ghc-debug client ? --- See GhcDebugMode. -ghcDebugMode :: GhcDebugMode -ghcDebugMode = -#ifdef GHCDEBUG - case debugLevel of - _ | not ghcDebugSupportedInLib -> GDNotSupported - (-1) -> GDNoPause - (-2) -> GDPauseAtStart - (-3) -> GDPauseAtEnd - _ -> GDDisabled - -- keep synced with GhcDebugMode -#else - GDNotSupported -#endif - --- | When ghc-debug support has been built into the program and enabled at runtime with --debug=-N, --- this calls ghc-debug's withGhcDebug; otherwise it's a no-op. -withGhcDebug' = -#ifdef GHCDEBUG - if ghcDebugMode > GDDisabled then withGhcDebug else id -#else - id -#endif - --- | When ghc-debug support has been built into the program, this calls ghc-debug's pause, otherwise it's a no-op. -ghcDebugPause' :: IO () -ghcDebugPause' = -#ifdef GHCDEBUG - pause -#else - return () -#endif - - -- | Trace (print to stderr) a string if the program debug level is at -- or above the specified level. At level 0, always prints. Otherwise, -- uses unsafePerformIO. @@ -484,10 +456,13 @@ dbg9MsgIO = dbgMsgIO 9 -- | Trace or log a label and showable value, pretty-printed, -- if the program debug level is at or above the specified level; -- then return the value. --- Traces to stderr or logs to a file depending on shouldLog. dbg :: (Show a) => Int -> String -> a -> a dbg = if shouldLog then plogAt else ptraceAt +-- | Alias for dbg, can be used to avoid namespace clashes. +dbg_ :: (Show a) => Int -> String -> a -> a +dbg_ = dbg + dbg0 :: Show a => String -> a -> a dbg0 = dbg 0 @@ -554,50 +529,53 @@ dbg9IO :: (MonadIO m, Show a) => String -> a -> m () dbg9IO = dbgIO 9 --- | Like dbgWith, but with a custom show function. +-- | Like dbg, but with a custom show function. dbgWith :: Int -> (a -> String) -> a -> a dbgWith = if shouldLog then logAtWith else traceAtWith --- | Like dbgN, but taking a show function instead of a label. dbg0With :: (a -> String) -> a -> a dbg0With = dbgWith 0 -dbg1With :: Show a => (a -> String) -> a -> a +dbg1With :: (a -> String) -> a -> a dbg1With = dbgWith 1 -dbg2With :: Show a => (a -> String) -> a -> a +dbg2With :: (a -> String) -> a -> a dbg2With = dbgWith 2 -dbg3With :: Show a => (a -> String) -> a -> a +dbg3With :: (a -> String) -> a -> a dbg3With = dbgWith 3 -dbg4With :: Show a => (a -> String) -> a -> a +dbg4With :: (a -> String) -> a -> a dbg4With = dbgWith 4 -dbg5With :: Show a => (a -> String) -> a -> a +dbg5With :: (a -> String) -> a -> a dbg5With = dbgWith 5 -dbg6With :: Show a => (a -> String) -> a -> a +dbg6With :: (a -> String) -> a -> a dbg6With = dbgWith 6 -dbg7With :: Show a => (a -> String) -> a -> a +dbg7With :: (a -> String) -> a -> a dbg7With = dbgWith 7 -dbg8With :: Show a => (a -> String) -> a -> a +dbg8With :: (a -> String) -> a -> a dbg8With = dbgWith 8 -dbg9With :: Show a => (a -> String) -> a -> a +dbg9With :: (a -> String) -> a -> a dbg9With = dbgWith 9 -- | Helper for producing debug messages: -- concatenates a name (eg a function name), -- short description of the value being logged, -- and string representation of the value. +-- +-- Eg: @let lbl = lbl_ "print"@, +-- @dbg1With (lbl "part 1".show) ...@. +-- lbl_ :: String -> String -> String -> String lbl_ name desc val = name <> ": " <> desc <> ":" <> " " <> val -- XXX the resulting function is constrained to only one value type --- -- | A new helper for defining a local "dbg" function. +-- -- | A helper for defining a local "dbg" function. -- -- Given a debug level and a topic string (eg, a function name), -- -- it generates a function which takes -- -- - a description string, @@ -606,8 +584,71 @@ lbl_ name desc val = name <> ": " <> desc <> ":" <> " " <> val -- -- debug-logs the topic, description and result of calling the show function on the value, -- -- formatted nicely, at the specified debug level or above, -- -- then returns the value. --- dbg_ :: forall a. Show a => Int -> String -> (String -> (a -> String) -> a -> a) +-- dbg_ :: forall a. Int -> String -> (String -> (a -> String) -> a -> a) -- dbg_ level topic = -- \desc showfn val -> -- dbgWith level (lbl_ topic desc . showfn) val -- {-# HLINT ignore "Redundant lambda" #-} + + +-- | Whether ghc-debug support is included in this build, and if so, how it will behave. +-- When hledger is built with the @ghcdebug@ cabal flag (off by default, because of extra deps), +-- it can listen (on unix ?) for connections from ghc-debug clients like ghc-debug-brick, +-- for pausing/resuming the program and inspecting memory usage and profile information. +-- +-- With a ghc-debug-supporting build, ghc-debug can be enabled by running hledger with +-- a negative --debug level. There are three different modes: +-- --debug=-1 - run normally (can be paused/resumed by a ghc-debug client), +-- --debug=-2 - pause and await client commands at program start (not useful currently), +-- --debug=-3 - pause and await client commands at program end. +data GhcDebugMode = + GDNotSupported + | GDDisabled + | GDNoPause + | GDPauseAtStart + | GDPauseAtEnd + -- keep synced with ghcDebugMode + deriving (Eq,Ord,Show) + +-- | Is the hledger-lib package built with ghc-debug support ? +ghcDebugSupportedInLib :: Bool +ghcDebugSupportedInLib = +#ifdef GHCDEBUG + True +#else + False +#endif + +-- | Should the program open a socket allowing control by ghc-debug-brick or similar ghc-debug client ? +-- See GhcDebugMode. +ghcDebugMode :: GhcDebugMode +ghcDebugMode = +#ifdef GHCDEBUG + case debugLevel of + _ | not ghcDebugSupportedInLib -> GDNotSupported + (-1) -> GDNoPause + (-2) -> GDPauseAtStart + (-3) -> GDPauseAtEnd + _ -> GDDisabled + -- keep synced with GhcDebugMode +#else + GDNotSupported +#endif + +-- | When ghc-debug support has been built into the program and enabled at runtime with --debug=-N, +-- this calls ghc-debug's withGhcDebug; otherwise it's a no-op. +withGhcDebug' = +#ifdef GHCDEBUG + if ghcDebugMode > GDDisabled then withGhcDebug else id +#else + id +#endif + +-- | When ghc-debug support has been built into the program, this calls ghc-debug's pause, otherwise it's a no-op. +ghcDebugPause' :: IO () +ghcDebugPause' = +#ifdef GHCDEBUG + pause +#else + return () +#endif