Skip to content

Call trace for failing evaluation via new emitter mode #7178

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Added

- Added a new emitter mode `logWithCallTraceEmitter` which uses trace messages generated by `PlutusTx.Plugin:profile-all` flag of plutus-tx-plugin to create call trace of the functions that led to the evaluation failure. If script passes or script is not compiled with `profile-all` flag, `logWithCallTraceEmitter` will behave as regular `logEmitter`.

9 changes: 7 additions & 2 deletions plutus-core/executables/src/PlutusCore/Executable/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,13 @@ data TimingMode = NoTiming | Timing Integer deriving stock (Eq) -- Report progra
data CekModel = Default | Unit -- Which cost model should we use for CEK machine steps?
data PrintMode = Classic | Simple | Readable | ReadableSimple deriving stock (Show, Read)
data NameFormat = IdNames | DeBruijnNames -- Format for textual output of names
data TraceMode = None | Logs | LogsWithTimestamps | LogsWithBudgets deriving stock (Show, Read)
data TraceMode
= None
| Logs
| LogsWithTimestamps
| LogsWithBudgets
| LogsWithCallTrace
deriving stock (Show, Read)
type ExampleName = T.Text
data ExampleMode = ExampleSingle ExampleName | ExampleAvailable

Expand Down Expand Up @@ -95,4 +101,3 @@ pirFormatToFormat FlatNamed = Flat Named

-- | Output types for some pir commands
data Language = PLC | UPLC

6 changes: 3 additions & 3 deletions plutus-core/executables/traceToStacks/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,10 @@ parseProfileEvent valIx (LogRow str vals) =
let val = vals !! (valIx-1)
in case words str of
[transition,var] ->
-- See Note [Profiling Marker]
case transition of
"entering" -> MkProfileEvent val Enter (T.pack var)
"exiting" -> MkProfileEvent val Exit (T.pack var)
"->" -> MkProfileEvent val Enter (T.pack var)
"<-" -> MkProfileEvent val Exit (T.pack var)
badLog -> error $
"parseProfileEvent: expecting \"entering\" or \"exiting\" but got "
<> show badLog
Expand Down Expand Up @@ -105,4 +106,3 @@ getStacks = go []
go [] [] = []
go stacks [] = error $
"getStacks; go: stack " <> show stacks <> " isn't empty but the log is."

Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ module UntypedPlutusCore.Evaluation.Machine.Cek
, logEmitter
, logWithTimeEmitter
, logWithBudgetEmitter
, logWithCallTraceEmitter
-- * Misc
, BuiltinsRuntime (..)
, CekValue (..)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module UntypedPlutusCore.Evaluation.Machine.Cek.EmitterMode
( noEmitter
, logEmitter
, logWithTimeEmitter
, logWithBudgetEmitter
) where
module UntypedPlutusCore.Evaluation.Machine.Cek.EmitterMode (
noEmitter,
logEmitter,
logWithTimeEmitter,
logWithBudgetEmitter,
logWithCallTraceEmitter,
) where

import UntypedPlutusCore.Evaluation.Machine.Cek.Internal

Expand Down Expand Up @@ -68,3 +70,36 @@ logWithBudgetEmitter = EmitterMode $ \getBudget -> do
let withBudget = logs <&> \str -> encodeRecord (str, exCpu, exMemory)
modifySTRef logsRef (`DList.append` withBudget)
pure $ CekEmitterInfo emitter (DList.toList <$> readSTRef logsRef)

{-| Emits log and, when script evaluation fails, call trace.

This requires script to be compiled with `PlutusTx.Plugin:profile-all` turned on because this relies
on compiler-generated trace calls that notifies entrance and exit of a function call. These traces
that mark entrance and exit are ordinary traces like "-> rob:Example.hs:3:1-3:15" and "<-
bob:Example.hs:1:1-1:13" with "->" and "<-" prefixies, where "bob" and "rob" is the name
of the function with source span. If regular script with no entrance/exit marker is given, this
emitter will behave identically to 'logEmitter'.

When script evaluation fails, this emitter will give call trace of the functions that led to the
evaluation failure. This is useful for pin-pointing specific area of the codebase that caused
failure when debugging a script. When script evaluation passes, every trace message generated by
`profile-all` flag will be removed, and this emitter will behave identically to 'logEmitter'.
-}
logWithCallTraceEmitter :: EmitterMode uni fun
logWithCallTraceEmitter = EmitterMode $ \_ -> do
logsRef <- newSTRef DList.empty
let
addTrace DList.Nil logs = logs
addTrace newLogs DList.Nil = newLogs
addTrace newLogs logs = DList.fromList $ go (DList.toList newLogs) (DList.toList logs)
where
go l [] = l
go [] l = l
go (x : xs) l =
-- See Note [Profiling Marker]
case (T.words (last l), T.words x) of
("->" : enterRest, "<-" : exitRest) | enterRest == exitRest -> go xs (init l)
_ -> go xs (l <> [x])

emitter logs = CekM $ modifySTRef logsRef (addTrace logs)
pure $ CekEmitterInfo emitter (DList.toList <$> readSTRef logsRef)
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ module UntypedPlutusCore.Evaluation.Machine.CommonAPI
, logEmitter
, logWithTimeEmitter
, logWithBudgetEmitter
, logWithCallTraceEmitter
-- * Misc
, CekValue(..)
, readKnownCek
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module UntypedPlutusCore.Evaluation.Machine.SteppableCek
, logEmitter
, logWithTimeEmitter
, logWithBudgetEmitter
, logWithCallTraceEmitter
-- * Misc
, CekValue(..)
, readKnownCek
Expand Down
1 change: 1 addition & 0 deletions plutus-executables/executables/uplc/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ runEval (EvalOptions inp ifmt printMode nameFormat budgetMode traceMode
Logs -> Cek.logEmitter
LogsWithTimestamps -> Cek.logWithTimeEmitter
LogsWithBudgets -> Cek.logWithBudgetEmitter
LogsWithCallTrace -> Cek.logWithCallTraceEmitter
-- Need the existential cost type in scope
let budgetM = case budgetMode of
Silent -> SomeBudgetMode Cek.restrictingEnormous
Expand Down
4 changes: 4 additions & 0 deletions plutus-tx-plugin/plutus-tx-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ test-suite plutus-tx-plugin-tests
BuiltinList.Budget.Spec
ByteStringLiterals.Lib
ByteStringLiterals.Spec
CallTrace.Lib
CallTrace.OtherModule
CallTrace.Spec
DataList.Budget.Spec
Inline.Spec
IntegerLiterals.NoStrict.NegativeLiterals.Spec
Expand Down Expand Up @@ -196,6 +199,7 @@ test-suite plutus-tx-plugin-tests
, plutus-tx ^>=1.48
, plutus-tx-plugin ^>=1.48
, plutus-tx:plutus-tx-testlib
, prettyprinter
, serialise
, tasty
, tasty-golden
Expand Down
51 changes: 38 additions & 13 deletions plutus-tx-plugin/src/PlutusTx/Compiler/Expr.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ import PlutusIR.Purity qualified as PIR
import PlutusCore qualified as PLC
import PlutusCore.Data qualified as PLC
import PlutusCore.MkPlc qualified as PLC
import PlutusCore.Pretty qualified as PP
import PlutusCore.StdLib.Data.Function qualified
import PlutusCore.Subst qualified as PLC

Expand Down Expand Up @@ -650,19 +649,32 @@ hoistExpr var t = do
(PIR.Def var' (PIR.mkVar var', PIR.Strict))
mempty

t' <- maybeProfileRhs var' =<< addSpan (compileExpr t)
t' <- maybeProfileRhs var var' =<< addSpan (compileExpr t)
-- See Note [Non-strict let-bindings]
PIR.modifyTermDef lexName (const $ PIR.Def var' (t', PIR.NonStrict))
pure $ PIR.mkVar var'

-- 'GHC.Var' in argument is only for extracting srcspan and accurate name.
maybeProfileRhs
:: (CompilingDefault uni fun m ann) => PLCVar uni -> PIRTerm uni fun -> m (PIRTerm uni fun)
maybeProfileRhs var t = do
:: (CompilingDefault uni fun m ann)
=> GHC.Var
-> PLCVar uni
-> PIRTerm uni fun
-> m (PIRTerm uni fun)
maybeProfileRhs ghcVar var t = do
CompileContext{ccOpts = compileOpts} <- ask
let ty = PLC._varDeclType var
varName = PLC._varDeclName var
displayName = T.pack $ PP.displayPlcSimple varName
isFunctionOrAbstraction = case ty of PLC.TyFun{} -> True; PLC.TyForall{} -> True; _ -> False
let
nameStr = GHC.occNameString $ GHC.occName $ GHC.varName $ ghcVar
displayName = T.pack $
case getVarSourceSpan ghcVar of
-- When module is not compiled and GHC is using cached build from previous build, it will
-- lack source span. There's nothing much we can do about this here since this is GHC
-- behavior. Issue #7203
Nothing -> nameStr
Just src -> nameStr <> " (" <> show (src ^. srcSpanIso) <> ")"

ty = PLC._varDeclType var
isFunctionOrAbstraction = case ty of PLC.TyFun{} -> True; PLC.TyForall{} -> True; _ -> False
-- Trace only if profiling is on *and* the thing being defined is a function
if coProfile compileOpts == All && isFunctionOrAbstraction
then do
Expand Down Expand Up @@ -765,6 +777,18 @@ entryExitTracingInside lamName displayName = go mempty
let ty' = PLC.typeSubstTyNames (\tn -> Map.lookup tn subst) ty
in entryExitTracing lamName displayName e ty'

{- Note [Profiling Markers]
The @profile-all@ will insert trarces when entering and exciting functions. These
traces have a string marker to indicate that a given traces message is for enter/exit
marking. Markers are just simple strings: "->" and "<-". So for any reason in the
future this marker needs to be changed, all of utilities that uses this marker will
need to be updated.

This list will track of all of the utilities that uses this marker:
- plutus-core:traceToStacks
- @UntypedPlutusCore.Evaluation.Machine.Cek.EmitterMode.logWithCallTraceEmitter@
-}

-- | Add tracing before entering and after exiting a term.
entryExitTracing
:: PLC.Name
Expand All @@ -780,9 +804,10 @@ entryExitTracing lamName displayName e ty =
annMayInline
( mkTrace
(PLC.TyFun annMayInline defaultUnitTy ty) -- ()-> ty
("entering " <> displayName)
-- See Note [Profiling Marker]
("-> " <> displayName)
-- \() -> trace @c "exiting f" e
(LamAbs annMayInline lamName defaultUnitTy (mkTrace ty ("exiting " <> displayName) e))
(LamAbs annMayInline lamName defaultUnitTy (mkTrace ty ("<- " <> displayName) e))
)
defaultUnit

Expand Down Expand Up @@ -1159,16 +1184,16 @@ compileExpr e = traceCompilation 2 ("Compiling expr:" GHC.<+> GHC.ppr e) $ do
_ -> compileTypeNorm $ GHC.varType b
-- See Note [Non-strict let-bindings]
withVarTyScoped b ty $ \v -> do
rhs'' <- maybeProfileRhs v rhs'
rhs'' <- maybeProfileRhs b v rhs'
let binds = pure $ PIR.TermBind annMayInline PIR.NonStrict v rhs''
body' <- compileExpr body
pure $ PIR.Let annMayInline PIR.NonRec binds body'
GHC.Let (GHC.Rec bs) body ->
withVarsScoped (fmap (second (const Nothing)) bs) $ \vars -> do
-- the bindings are scope in both the body and the args
-- TODO: this is a bit inelegant matching the vars back up
binds <- for (zip vars bs) $ \(v, (_, rhs)) -> do
rhs' <- maybeProfileRhs v =<< compileExpr rhs
binds <- for (zip vars bs) $ \(v, (ghcVar, rhs)) -> do
rhs' <- maybeProfileRhs ghcVar v =<< compileExpr rhs
-- See Note [Non-strict let-bindings]
pure $ PIR.TermBind annMayInline PIR.NonStrict v rhs'
body' <- compileExpr body
Expand Down
12 changes: 12 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func01.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
func 1
-> nestedLinear (test/CallTrace/Spec.hs:71:1-71:12)
-> nestedLinear2 (test/CallTrace/Spec.hs:72:1-72:13)
-> nestedLinear3 (test/CallTrace/Spec.hs:73:1-73:13)
-> nestedLinear4 (test/CallTrace/Spec.hs:74:1-74:13)
-> error
10 changes: 10 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func02.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This encoding :78:1-78:4 confuses me: what does each section mean ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from line 78 column 1 to line 78 column 4. this is what SrcSpan pretty printer does. I don't really want to change the pretty instance.

Copy link
Contributor

@Unisay Unisay Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see!
My first attempt to parse it in the head produced this result:

  1. test/CallTrace/Spec.hs
  2. 78
  3. 1-78
  4. 4

🤣

I think that my mental parser trips over the : as a separator between the file path (LHS) and SrcSpan (RHS).
I agree that we don't want to change the way SrcSpan is printed here,
but we can avoid mixing the same char : to separate

  1. File path / Src Span
  2. Line / Column

I suggested to use @ as a 1. separator before.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't change this without changing Pretty instances

Just src -> nameStr <> " (" <> show (src ^. srcSpanIso) <> ")"

I think this is okay honestly. This is standard format used by GHC(and other languages as well!), e.g. test/CallTrace/Spec.hs:88:10

-> nestedLinear2 (test/CallTrace/Spec.hs:72:1-72:13)
-> nestedLinear3 (test/CallTrace/Spec.hs:73:1-73:13)
-> nestedLinear4 (test/CallTrace/Spec.hs:74:1-74:13)
-> error
9 changes: 9 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func03.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> functionFromOtherModule (test/CallTrace/Spec.hs:93:1-93:23)
-> $
-> errorWhenTrue (test/CallTrace/OtherModule.hs:9:1-9:13)
8 changes: 8 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func04.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $cmyClassFunc (test/CallTrace/Spec.hs:99:3-99:13)
-> error
10 changes: 10 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func05.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $cmyClassFunc (test/CallTrace/Spec.hs:99:3-99:13)
-> functionFromOtherModule (test/CallTrace/Spec.hs:93:1-93:23)
-> $
-> errorWhenTrue (test/CallTrace/OtherModule.hs:9:1-9:13)
8 changes: 8 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func06.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $cmyClassFunc (test/CallTrace/Spec.hs:103:3-103:13)
-> error
7 changes: 7 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func07.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $fMyClassInOtherModuleInteger_$cmyClassFuncInOtherModule (test/CallTrace/OtherModule.hs:19:3-19:26)
7 changes: 7 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func08.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $fMyClassInOtherModuleInteger_$cmyClassFuncInOtherModule (test/CallTrace/OtherModule.hs:19:3-19:26)
7 changes: 7 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func09.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> $fMyClassInOtherModule()_$cmyClassFuncInOtherModule (test/CallTrace/OtherModule.hs:23:3-23:26)
7 changes: 7 additions & 0 deletions plutus-tx-plugin/test/CallTrace/9.6/func42.eval.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> func (test/CallTrace/Spec.hs:78:1-78:4)
-> error
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> functionFromOtherModule (test/CallTrace/Spec.hs:93:1-93:23)
-> $
-> errorWhenTrue (test/CallTrace/OtherModule.hs:9:1-9:13)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> nestedLinear (test/CallTrace/Spec.hs:71:1-71:12)
-> nestedLinear2 (test/CallTrace/Spec.hs:72:1-72:13)
-> nestedLinear3 (test/CallTrace/Spec.hs:73:1-73:13)
-> nestedLinear4 (test/CallTrace/Spec.hs:74:1-74:13)
-> error
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
An error has occurred:
The machine terminated because of an error, either from a built-in function or from an explicit use of 'error'.
Caused by: error

Trace:
-> nestedLinear (test/CallTrace/Spec.hs:71:1-71:12)
-> nestedLinear2 (test/CallTrace/Spec.hs:72:1-72:13)
-> nestedLinear3 (test/CallTrace/Spec.hs:73:1-73:13)
-> nestedLinear4 (test/CallTrace/Spec.hs:74:1-74:13)
-> error
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CPU: 4_513_072
Memory: 23_548
Term Size: 176
Flat Size: 530

No Trace Produced

(con string "hi")
Loading