Getting a stack trace in Daml Script

I’m trying to write helper functions we can use in tests written in Daml Script, however annoyingly when there is a failure the error message points to the line inside the helper function only, without a stack trace, which means we need to find the location of the error inside the test by hand. I tried adding HasCallStack to the helper functions to no avail (maybe it doesn’t work with Script?).

Here is a minimal example demonstrating this:

{-# LANGUAGE AllowAmbiguousTypes #-}

module ScriptTrace where

import Daml.Script
import DA.Stack


template Asset with
    assetParty : Party
  where
    signatory assetParty
    choice Expire : ()
        controller assetParty
        do abort "boom"

test = do
    assetParty <- allocateParty "party"

    submit assetParty do
        createCmd Asset with assetParty

    exerciseByParty @Asset @"assetParty" assetParty Expire

-- Test functions

type Templ t = (Template t, HasAgreement t)
type TemplWField t pf = (Templ t, HasField pf t Party)

-- | The 'getByParty' function returns the contract id and contract of type 't' that is
--   visible to party 'p' and where the value of field 'pf' equals to 'p'.
--   It is assumed that only one contract like this exists.
getByParty : forall t pf. (HasCallStack, TemplWField t pf) => Party -> Script (ContractId t, t)
getByParty p = do
    cs <- query @t p
    case filter (\(_, t) -> getField @pf t == p) cs of
        [c] -> return c
        [] -> abort "getByParty: contract not found"
        _ -> abort "getByParty: more than one contract exists"

exerciseByParty : forall t pf c r. (HasCallStack, TemplWField t pf, Choice t c r) => Party -> c -> Script r
exerciseByParty p c = do
    (cid, _) <- getByParty @t @pf p
    submit p $ exerciseCmd cid c

And the script result:

Script execution failed on commit at ScriptTrace:44:5:
  Unhandled exception:  DA.Exception.GeneralError:GeneralError@86828b9843465f419db1ef8a8ee741d1eef645df02375ebf509cdc8c3ddd16cb with
                          message = "boom"

Line 44 is the last line of exerciseByParty which is not exactly helpful.

Is there any way to get a stack trace here?

Thank you!

There are two separate call stacks:

  1. The transaction tree “callstack”, which you can inspect in the transaction view.
  2. The purely functional callstack from the last exercise node, which you can get through HasCallStack and callStack.

I’ve modified your example a little here to demonstrate both:

recurse : forall p . (HasCallStack) => Int -> p
recurse n = case n of
  0 -> error (prettyCallStack callStack)
  _ -> recurse (n-1)

template Asset with
    assetParty : Party
  where
    signatory assetParty
    nonconsuming choice Expire : ()
        controller assetParty
        do exercise self Inner
    choice Inner : ()
        controller assetParty
        do recurse 5

This results in this output:

Script execution failed on commit at [Setup:53:5](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2052%2C%2052%5D):
Unhandled exception: DA.Exception.GeneralError:GeneralError@86828b9843465f419db1ef8a8ee741d1eef645df02375ebf509cdc8c3ddd16cb with
message =
"CallStack (from HasCallStack):
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:10:7 in main:Setup
recurse, called at .../Setup.daml:21:11 in main:Setup"

Ledger time: 1970-01-01T00:00:00Z

Partial transaction:
Failed exercise (unknown source):
exercises Inner on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))
with
Sub-transactions:
0
└─> 'party' exercises Expire on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))
children:
1
└─> 'party' exercises Inner on #0:0 ([Setup:Asset](command:daml.revealLocation?%5B%22file%3A%2F%2F%2FUsers%2Fbame%2Ftest%2Fcreate-daml-app%2Fdaml%2FSetup.daml%22%2C%2012%2C%2013%5D))

So you get three pieces of information:

  1. The commit that failed was the one in the setup script at line 53 - exerciseByParty @Asset @"assetParty" assetParty Expire
  2. The transaction got as far as calling Expire and then Inner. The error occurred in the body of Inner on the Asset template.
  3. The error occurred after calling recurse a bunch of times.

There is no doubt additional information that would help:

  1. Line numbers on the partial transaction to see where one exercise called another.
  2. An entry in the call stack that points to the actual location of the call to callStack.

Note that the reason callStack does not cross exercise boundaries is privacy. If Inner and Expire had different stakeholder sets, calling callStack in Inner would leak information.

1 Like

I think I wasn’t clear enough about my issue. In this instance I’m not even interested about the calls inside the choice. I would just like to know exactly which line in the test script caused the error. I’m not sure how exactly you modified my example but pasting recurse above Asset and adding the other choice to Asset led to submit p $ exerciseCmd cid c inside exerciseByParty move to around line 53 which is referenced in your example error output.
This is exactly my problem, that it is pointing inside the helper function which is likely used many times inside a test.

Let’s say I have a test like:

test = do
    assetParty <- allocateParty "party"

    submit assetParty do
        createCmd Asset with assetParty

    exerciseByParty @Asset @"assetParty" assetParty SomeChoice
    exerciseByParty @Asset @"assetParty" assetParty SomeChoice
    exerciseByParty @Asset @"assetParty" assetParty SomeChoice
    exerciseByParty @Asset @"assetParty" assetParty SomeChoice
    exerciseByParty @Asset @"assetParty" assetParty SomeChoice

Knowing the error happened inside exerciseByParty is not going to be any help locating where in the test the error happened.