Skip to content

[WIP] Improved error handling for template instantiation#12449

Open
bendavid wants to merge 15 commits into
root-project:masterfrom
bendavid:template_error_handling
Open

[WIP] Improved error handling for template instantiation#12449
bendavid wants to merge 15 commits into
root-project:masterfrom
bendavid:template_error_handling

Conversation

@bendavid

@bendavid bendavid commented Mar 8, 2023

Copy link
Copy Markdown
Contributor

Two substantive changes:

  1. Explicitly catch errors in pyroot when the wrapper function fails to compile (this is actually an expanded version of a partial fix which is already upstream in cppyy: wlav/cppyy-backend@8de6ed5)
  2. Make sure template instantiation fails by catching clang errors within LookupHelper and rolling back the transaction where appropriate (still not entirely sure this is exactly the right fix, @Axel-Naumann @jalopezg-git please take a look)
  3. Implement a mechanism for redirecting cling diagnostics to a user provided ostream and use this in cppyy to capture the diagnostic output and append it to the python exceptions or warnings as appropriate

This PR fixes #11854

There are still some remaining problems with the transaction rollback, however template instantiation from cppyy now behaves the same as calling TInterpreter::Declare in this respect. This is likely related to the issues described by @jalopezg-git in #12449 (comment) and can be fixed in a future PR.

Consider the following test case:

test.h:

template <typename T>
class Helper {

public:

  Helper() {}

  std::size_t operator() () const {
    const std::size_t res = 0;
    res = T{0, 0}.size();
    return res;
  }

};

template <typename H>
std::size_t call_helper(const H &helper) {
  return helper();
}

test.py

import ROOT

ret = ROOT.gInterpreter.Declare('#include "test.h"')
print("declare ret", ret)

print("creating helper")
helper = ROOT.Helper[ROOT.std.vector["double"]]()

print("calling helper")

for i in range(2):
   print(f"call attempt {i}")
   try:
      res = ROOT.call_helper(helper)
      print("helper call succeeded:", res)
   except Exception as e:
      print("helper call failed")
      print(e)

The output below is now close to optimal for the first instantiation attempt. On the second instantiation attempt the error message is different/less useful because of the imperfect transaction rollback already noted. (but the same happens instantiating the template through TInterpreter::Declare as said)

declare ret True
creating helper
calling helper
call attempt 0
helper call failed
Template method resolution failed:
  Failed to instantiate "call_helper(Helper<vector<double> >&)"
    In file included from input_line_52:1:
/home/b/bendavid/pyrootdebug6/test.h:10:9: error: cannot assign to variable 'res' with const-qualified type 'const std::size_t' (aka 'const unsigned long')
    res = T{0, 0}.size();
    ~~~ ^
/home/b/bendavid/pyrootdebug6/test.h:18:10: note: in instantiation of member function 'Helper<std::vector<double, std::allocator<double> > >::operator()' requested here
  return helper();
         ^
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here
/home/b/bendavid/pyrootdebug6/test.h:9:23: note: variable 'res' declared const here
    const std::size_t res = 0;
    ~~~~~~~~~~~~~~~~~~^~~~~~~

  Failed to instantiate "call_helper(Helper<vector<double> >*)"
    error: called object type 'Helper<std::vector<double, std::allocator<double> > > *' is not a function or function pointer
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > *>' requested here

  Failed to instantiate "call_helper(Helper<vector<double> >)"
    error: type 'const Helper<std::vector<double, std::allocator<double> > >' does not provide a call operator
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here

call attempt 1
helper call failed
Template method resolution failed:
  Failed to instantiate "call_helper(Helper<vector<double> >&)"
    error: type 'const Helper<std::vector<double, std::allocator<double> > >' does not provide a call operator
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here

  Failed to instantiate "call_helper(Helper<vector<double> >*)"
    error: called object type 'Helper<std::vector<double, std::allocator<double> > > *' is not a function or function pointer
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > *>' requested here

  Failed to instantiate "call_helper(Helper<vector<double> >)"
    error: type 'const Helper<std::vector<double, std::allocator<double> > >' does not provide a call operator
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here

(on the console the output also has the nice highlighting and colors one would normally get from clang diagnostic printing, see screenshot below)

Needless to say, taken together this constitutes a major improvement when trying to use complex templated code with pyroot/cppyy

image

@phsft-bot

Copy link
Copy Markdown

Starting build on ROOT-debian10-i386/soversion, ROOT-performance-centos8-multicore/cxx17, ROOT-ubuntu18.04/nortcxxmod, ROOT-ubuntu2004/python3, mac12/noimt, mac11/cxx14, windows10/cxx14
How to customize builds

@bendavid bendavid force-pushed the template_error_handling branch from b8e1515 to 1e89113 Compare March 8, 2023 03:45
@Axel-Naumann

Copy link
Copy Markdown
Member

The failure to unload broken declarations (@jalopezg-git FYI), does that still happen after this PR, or is this addressed by the PR? I'm not sure I understand how much of the PR description describes this PR vs what's left to be done?

@Axel-Naumann Axel-Naumann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks a lot for your work on this. Can you remind me what typical new failures would look like if instead we were to repeat the lookup without diagnostic suppression, in those cases where cppyy cannot find a suitable function to call?

Comment thread interpreter/cling/lib/Interpreter/LookupHelper.cpp Outdated
@bendavid

bendavid commented Mar 8, 2023

Copy link
Copy Markdown
Contributor Author

All of the code example/output in the PR description corresponds to with this PR included.

Repeating the lookup without diagnostic suppression doesn't give the correct error message again
(this corresponds to the "call attempt 1" case in the output from test.py in the PR description)

ie with superfluous debug output snipped out:

declare ret True
creating helper
calling helper
call attempt 0
In file included from input_line_52:1:
/home/b/bendavid/pyrootdebug3/test.h:10:9: error: cannot assign to variable 'res' with const-qualified type 'const std::size_t' (aka 'const unsigned long')
    res = T{0, 0}.size();
    ~~~ ^
/home/b/bendavid/pyrootdebug3/test.h:18:10: note: in instantiation of member function 'Helper<std::vector<double, std::allocator<double> > >::operator()' requested here
  return helper();
         ^
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here
/home/b/bendavid/pyrootdebug3/test.h:9:23: note: variable 'res' declared const here
    const std::size_t res = 0;
    ~~~~~~~~~~~~~~~~~~^~~~~~~
/home/b/bendavid/pyrootdebug3/test.h:18:10: error: called object type 'Helper<std::vector<double, std::allocator<double> > > *' is not a function or function pointer
  return helper();
         ^~~~~~
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > *>' requested here
helper call failed
Template method resolution failed:
  Failed to instantiate "call_helper(Helper<vector<double> >&)"
  Failed to instantiate "call_helper(Helper<vector<double> >*)"
  Failed to instantiate "call_helper(Helper<vector<double> >)"
call attempt 1
/home/b/bendavid/pyrootdebug3/test.h:18:10: error: called object type 'Helper<std::vector<double, std::allocator<double> > > *' is not a function or function pointer
  return helper();
         ^~~~~~
note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > *>' requested here
helper call failed
Template method resolution failed:
  Failed to instantiate "call_helper(Helper<vector<double> >&)"
  Failed to instantiate "call_helper(Helper<vector<double> >*)"
  Failed to instantiate "call_helper(Helper<vector<double> >)"

So the relevant error message error: cannot assign to variable 'res' with const-qualified type 'const std::size_t' (aka 'const unsigned long') only appears in the first attempt (and is only printed because I've set gDebug=6 here)

@bendavid

bendavid commented Mar 9, 2023

Copy link
Copy Markdown
Contributor Author

Failure for ubuntu20 build looks possibly unrelated to this PR.

@bendavid

Copy link
Copy Markdown
Contributor Author

So in fact there is already a problem with rolling back the transaction even when just using TInterpreter::Declare:

test.h

template <typename T>
class Helper {

public:

  Helper() {}

  std::size_t operator() () const {
    const std::size_t res = 0;
    res = T{0, 0}.size();
    return res;
  }

};

template <typename H>
std::size_t call_helper(const H &helper) {
  return helper();
}

testdeclare.py

import ROOT

ret = ROOT.gInterpreter.Declare('#include "test.h"')
print("header include ret", ret)

print("creating helper")
helper = ROOT.Helper[ROOT.std.vector["double"]]()

bad_template = "template std::size_t call_helper<Helper<std::vector<double>>>(const Helper<std::vector<double>>&);"

for i in range(2):
    print(f"declare attempt {i}")
    ret = ROOT.gInterpreter.Declare(bad_template)
    print("ret", ret)

output:

header include ret True
creating helper
declare attempt 0
In file included from input_line_52:1:
/home/b/bendavid/pyrootdebug3/test.h:10:9: error: cannot assign to variable 'res' with const-qualified type 'const std::size_t' (aka 'const unsigned long')
    res = T{0, 0}.size();
    ~~~ ^
/home/b/bendavid/pyrootdebug3/test.h:18:10: note: in instantiation of member function 'Helper<std::vector<double, std::allocator<double> > >::operator()' requested here
  return helper();
         ^
/home/b/bendavid/pyrootdebug3/test.h:9:23: note: variable 'res' declared const here
    const std::size_t res = 0;
    ~~~~~~~~~~~~~~~~~~^~~~~~~
ret False
declare attempt 1
/home/b/bendavid/pyrootdebug3/test.h:18:10: error: type 'const Helper<std::vector<double, std::allocator<double> > >' does not provide a call operator
  return helper();
         ^~~~~~
input_line_55:1:22: note: in instantiation of function template specialization 'call_helper<Helper<std::vector<double, std::allocator<double> > > >' requested here
template std::size_t call_helper<Helper<std::vector<double>>>(const Helper<std::vector<double>>&);
                     ^
ret False

So again the error message is different/more obscure on the second attempt at explicit template instantiation.

@jalopezg-git

jalopezg-git commented Mar 14, 2023

Copy link
Copy Markdown
Contributor

The failure to unload broken declarations (@jalopezg-git FYI), does that still happen after this PR, or is this addressed by the PR? I'm not sure I understand how much of the PR description describes this PR vs what's left to be done?

I don't know how this PR relates to the unloading issues in cling. What I saw in the past is that DeclUnloader is buggy; specifically, it always removes declarations from the AST when that's not always appropriate. One case in which this fails is for members of a templated class (which clang initially marks as "pending instantiation").
If those are instantiated implicitly as part of a transaction that fails, DeclUnloader removes the member declaration. This prevents the decl from being re-emitted when needed. Instead, it should be left in the previous state and marked as "pending instantiation" again.
I have some code that should fix this (which coincides with most if not all the reported unloading issues). I will clean it and open a PR as soon as I finish the current on-going RNTuple work. 🙂

@bendavid

Copy link
Copy Markdown
Contributor Author

After modifying the logic to catch the error and fail the transaction rather than unloading the decl directly, repeated attempts at template instantiation from pyroot now behaves similarly to with TInterpreter::Declare. The remaining problems with incomplete rollback are almost certainly related to the issue which @jalopezg-git referred to, and can be fixed by his forthcoming PR.

Still TODO for this PR:
Capture and print the relevant errors and warnings during template instantiation.

@github-actions

github-actions Bot commented Mar 19, 2023

Copy link
Copy Markdown

Test Results

    22 files      22 suites   3d 19h 19m 38s ⏱️
 3 781 tests  3 776 ✅ 0 💤  5 ❌
81 254 runs  81 200 ✅ 0 💤 54 ❌

For more details on these failures, see this check.

Results for commit dee2fb7.

♻️ This comment has been updated with latest results.

@bendavid bendavid force-pushed the template_error_handling branch from ac63baf to 59c2de3 Compare March 21, 2023 14:04
@bendavid bendavid requested a review from pcanal as a code owner March 21, 2023 14:04
@bendavid bendavid force-pushed the template_error_handling branch 2 times, most recently from 2aa7e7a to 4e29a95 Compare March 21, 2023 18:01
@bendavid

Copy link
Copy Markdown
Contributor Author

The failure in tutorial-roofit-rf408_RDataFrameToRooFit-py is actually a real error which wasn't being caught before (a rather subtle SFINAE problem)

@bendavid

Copy link
Copy Markdown
Contributor Author

PR and description updated addressing also the diagnostic capture and printing.

I'm still not totally sure about how the catching of errors and rollback of the transaction is handled in LookupHelper as said in the description.

@bendavid

Copy link
Copy Markdown
Contributor Author

Any ideas on the remaining windows failure would also be welcome (it doesn't happen on linux and I don't have a windows setup to test with at the moment)

Comment thread interpreter/cling/lib/Interpreter/LookupHelper.cpp

@jalopezg-git jalopezg-git left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the contribution (and sorry for the delay in the review), @bendavid! 🙂 I have attached some comments to further improve the PR.

Regarding the unloading errors that I mentioned in one of my previous comments, I'll open a pull request soon.

Comment thread core/metacling/src/TClingDiagnostics.h
Comment thread core/meta/inc/TInterpreter.h
Comment thread core/metacling/src/TCling.cxx
Comment thread core/metacling/src/TCling.cxx
Comment thread core/metacling/src/TClingClassInfo.h
Comment thread interpreter/cling/lib/Interpreter/LookupHelper.cpp
Comment thread roofit/RDataFrameHelpers/inc/RooAbsDataHelper.h Outdated
Comment thread bindings/pyroot/cppyy/cppyy-backend/clingwrapper/src/clingwrapper.cxx Outdated
Comment thread bindings/pyroot/cppyy/CPyCppyy/src/TemplateProxy.cxx Outdated
@phsft-bot

Copy link
Copy Markdown

Build failed on windows10/cxx14.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Failing tests:

@jalopezg-git

Copy link
Copy Markdown
Contributor

I think this PR stalled for a while; perhaps we can give it a push and finally merge in the coming weeks?

@bendavid

Copy link
Copy Markdown
Contributor Author

Yes ok I can come back to this next week.

@jalopezg-git

Copy link
Copy Markdown
Contributor

There are still some remaining problems with the transaction rollback, however template instantiation from cppyy now behaves the same as calling TInterpreter::Declare in this respect. This is likely related to the issues described by @jalopezg-git in #12449 (comment) and can be fixed in a future PR.

FYI, #13565 should fix the issues with unloading that I mentioned before in this PR! I still need to look at two test failures, but it's mostly there 🙂!

@jalopezg-git

Copy link
Copy Markdown
Contributor

There are still some remaining problems with the transaction rollback, however template instantiation from cppyy now behaves the same as calling TInterpreter::Declare in this respect. This is likely related to the issues described by @jalopezg-git in #12449 (comment) and can be fixed in a future PR.

FYI, #13565 should fix the issues with unloading that I mentioned before in this PR! I still need to look at two test failures, but it's mostly there 🙂!

@bendavid #13565 was recently merged into master. Provided that you have the time, you could rebase this PR and see how it works now.

@bendavid

bendavid commented Oct 4, 2024

Copy link
Copy Markdown
Contributor Author

(Updated to fix code formatting)

@ferdymercury

Copy link
Copy Markdown
Collaborator

@wlav What do you think of the cppyy related changes?

@wlav

wlav commented Aug 6, 2025

Copy link
Copy Markdown
Contributor

From a quick look: indentation is all over the place (4 -> 3; tabs; missing indentation) and some of the new error messages are very verbose, which will look bad on a simple Python command prompt. The addition of a do-nothing ostringstream in the C api could do with a comment.

Of the new functionality, the diagnostics are probably fine, but some of the newly added error setting (e.g in Instantiate) may break tests (would have to see).

The one thing that can't happen is CallConstructor throwing exceptions. It makes it impossible to use from the C api.

@guitargeek

Copy link
Copy Markdown
Contributor

Thank you for commenting @wlav!

@bendavid, do you still want to work on this PR yourself? Or should I take it over and bring it to a mergeable state?

@bendavid

Copy link
Copy Markdown
Contributor Author

@guitargeek I'm on vacation until Aug. 27. I can look at this again when I'm back, but you're also welcome to take this over.

In case it helps I had rebased this on top of 6.34.04 a little while ago, so some of the conflicts may already be solved in https://github.com/bendavid/root/tree/v6-34-04-patches

@vepadulano vepadulano force-pushed the template_error_handling branch from 8b22039 to d9e2515 Compare November 29, 2025 13:38
@vepadulano vepadulano requested a review from bellenot as a code owner November 29, 2025 13:40
@vepadulano

Copy link
Copy Markdown
Member

@bendavid I've taken the liberty of rebasing the branch on latest master, make some little adaptations to address review comments, and add a test.

Comment on lines +2123 to +2126
R__WRITE_LOCKGUARD(ROOT::gCoreMutex);

const std::string diagnosticsold = diagnostics.str();
TInterpreter::RedirectDiagnostics redirectRAII(gInterpreter, diagnostics, /*enableColors*/ true, /*indent*/ 4);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder how this interacts with the future rebase of clingwrapper on top of CppInterop @aaronj0 @Vipul-Cariappa . Do we have similar infrastructure for retrieving interpreter diagnostics without leaking implementation details here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We currently do not have a way of doing this. But it should be relatively easy to implement. The question of "without leaking implementation details", not sure about that. If the question is: Can we propagate why a template instantiation fails? Yes, we propagate the reason.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe a clearer rephrasing of my question would be "can we obtain the same interpreter diagnostics in clingwrapper.cxx, without using any ROOT facility (i.e. R__WRITE_LOCKGUARD, ROOT::gCoreMutex, TInterpreter in this case), so that this diagnostic can become something generic and still give us the chance one day of outsourcing clingwrapper.cxx?"

@aaronj0 aaronj0 Dec 1, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"can we obtain the same interpreter diagnostics in clingwrapper.cxx, without using any ROOT facility (i.e. R__WRITE_LOCKGUARD, ROOT::gCoreMutex, TInterpreter in this case)

The way I see it, we will still need the gCore mutex for the clingwrapper API to continue supporting ROOT threading model. Which is not a problem since we still link against Core in the design that migrates to InterOp.

so that this diagnostic can become something generic and still give us the chance one day of outsourcing clingwrapper.cxx

As Vipul mentioned, obtaining the diagnostic itself is not yet supported, but it can indeed be done generically, i.e., based on Clang's diagnostics engine (like this PR does). Based on that, I would comment that the right place for the very nice TClingDiagnosticsRAII implemented by this PR is probably CppInterOp, which solves the concern on whether this can be generalized and outsourced. In my view, clingwrapper is the one part that will/should contain ROOT-specific extensions on top of CppInterOp. @vgvassilev can comment on the changes in Cling's LookupHelper, but on first glance, that looks good to me.

One way to proceed is to keep the current approach and, as part of the migration, upstream the diagnostics from TCling to CppInterOp, and revert the changes in TInterpreter, since we can then interact with the Cpp::Interpreter directly. If the need for this PR is not immediate, the easier option is to revisit it on top of the migration PR. Ideally, we shouldn't grow further the interactions between Core and the interpreter via metacling for cppyy use cases. Apart from that, this PR heads in the right direction and proposes some quite nice additions.

@vepadulano vepadulano closed this Nov 30, 2025
@vepadulano vepadulano reopened this Nov 30, 2025
@vepadulano

Copy link
Copy Markdown
Member

Just to keep track of my advancements, here is my plan regarding this PR to address the problem properly while ensuring a sustainable transition:

  1. Separate the changes that go into LookupHelper.cpp into a separate PR
  2. Then prepare a PR for CppInterop that contains most of the changes in core/metacling to allow propagating the diagnostics
  3. Test the new facility in CppInterop in clingwrapper.cxx, with the aim of avoiding any ROOT usage in clingwrapper
  4. Finally, adapt CPyCppyy to use the new facility

This will require some more time 🚧

vepadulano added a commit to vepadulano/root that referenced this pull request Jan 19, 2026
This is the first part of root-project#12449

Co-authored-by: Josh Bendavid <Josh.Bendavid@cern.ch>
vepadulano added a commit to vepadulano/root that referenced this pull request Jan 19, 2026
This is the first part of root-project#12449

Co-authored-by: Josh Bendavid <Josh.Bendavid@cern.ch>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bad error handling with pyroot template instantiations