Skip to content

Roll back a rejected XML load as a whole, and keep Tree's destructor from throwing - #1289

Merged
facontidavide merged 5 commits into
BehaviorTree:masterfrom
Quchaosheng:fix-1282-1281-clean
Nov 12, 2026
Merged

facontidavide merged 5 commits into
BehaviorTree:masterfrom
Quchaosheng:fix-1282-1281-clean

Conversation

@Quchaosheng

@Quchaosheng Quchaosheng commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Two fixes for #1282 and #1281.

#1282 - #1266 dropped a document that failed to load, but the rollback
stopped at the document boundary. If main.xml includes sub.xml and then
fails, sub.xml has already registered its trees and they stay behind;
sub.xml also becomes the first document, so instantiateTree() reads
main_tree_to_execute from it. Take a snapshot of
tree_roots/tree_sources/suffix_count before the call and restore it on
failure. That also puts back a tree the call redefined, whose element lives in
the document about to be erased.

#1281 - Tree::~Tree() calls haltTree(), and halting may throw: a
TryCatch with catch_on_halt="true" ticks its catch child, and a tick is
allowed to throw. An exception leaving a destructor calls std::terminate, so
destroying such a tree aborted the process. Mark the destructor noexcept and
swallow the exception.

Both come with tests that fail on the current master:

  • XMLParserTest.RejectedIncludeKeepsNoTree and
    RejectedInlineIncludesKeepNoTree see the included tree survive a rejected load.
  • TreeDestructorTest.HaltThatThrowsDoesNotTerminate aborts with
    terminate called after throwing an instance of 'BT::NodeExecutionError'.

Full suite: 684 passed, 2 skipped. The XML side is also clean under
-fsanitize=address.

The noexcept on Tree::~Tree() changes a public header. If you would rather
keep the signature alone, dropping the keyword and keeping the try/catch in the
body is enough - say so and I will change it.


Maintainer changes (rebased on master after #1290)

  • One rollback point. <include> now calls loadDocImpl directly, and only the top-level load call snapshots and restores. On failure it drops every document the call opened (erase(doc_it, end())). Before, a valid included document stayed behind as the "first document" whose main_tree_to_execute a later load picked up. The snapshot also covers subtree_models and current_path, as listed in XMLParser: a rejected file's valid includes stay registered #1282.
  • ~Tree(). The noexcept is gone (destructors are implicitly noexcept). A throwing halt is now reported on stderr, as FinallyNode does, instead of being swallowed silently.
  • Tests.
    • The temp-dir helper is shared through tests/test_helper.hpp, which removes the getpid call that failed on MSVC.
    • The destructor test moved next to the other catch_on_halt tests and checks that the catch child ran.
    • New RejectedIncludeIsNotTheFirstDocument test.
    • The redefinition test now goes through a valid include of a rejected file; the previous version passed on master.

Behavior change: after a rejected load, trees from its valid includes are no longer registered. A load either succeeds completely or leaves the parser as it was.

Follow-up, not in this PR: a node that throws while halting still stops haltTree() before it reaches the remaining nodes. The PR prevents std::terminate, but sibling nodes may miss onHalted().

Quchaosheng and others added 3 commits November 12, 2026 22:00
BehaviorTree#1266 dropped a document that failed to load, but the rollback stopped at the
document boundary. If main.xml includes sub.xml and then fails, sub.xml has
already registered its trees and they stay behind, and sub.xml becomes the
first document, so instantiateTree() reads main_tree_to_execute from it.

Take a snapshot before the call and restore it on failure. That also puts back
a tree this call redefined, whose element lives in the document about to be
erased.

Fixes BehaviorTree#1282
Tree::~Tree() calls haltTree(), and halting may throw: a TryCatch with
catch_on_halt="true" ticks its catch child, and a tick is allowed to throw.
An exception leaving a destructor calls std::terminate, so destroying such a
tree aborted the process.

Mark the destructor noexcept and swallow the exception. There is no caller
left to report to.

Fixes BehaviorTree#1281
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@facontidavide

Copy link
Copy Markdown
Collaborator

let me ask this.

Is this a REAL problem YOU have in PRACTICE, or something reported by AI?

I don't have any problem with AI, but I feel like this is solving a problem nobody has

- loadLastDocImpl is now only the top-level rollback point: <include> calls
  loadDocImpl directly, and on failure the call drops every document it
  opened (erase(doc_it, end())), so a valid included document no longer stays
  the "first document" whose main_tree_to_execute instantiateTree() reads.
  The snapshot also covers subtree_models and current_path, as listed in
  BehaviorTree#1282, and is restored by move.
- ~Tree(): drop the redundant noexcept (destructors are implicitly noexcept)
  and report a throwing halt on stderr, as FinallyNode does, instead of
  swallowing it silently (also fixes bugprone-empty-catch).
- Tests: share TempSubdir/writeFile through test_helper.hpp (no getpid, which
  broke MSVC); move the destructor test next to the other catch_on_halt tests
  using lambda nodes and assert the catch child ran; add a first-document
  test; make the redefinition test go through a valid include of a rejected
  file (the old one passed on master); drop HaltStillRuns and
  RollbackKeepsEarlierTrees, which asserted nothing new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@facontidavide
facontidavide merged commit 0ffae1c into BehaviorTree:master Nov 12, 2026
17 checks passed
@Quchaosheng

Copy link
Copy Markdown
Contributor Author

let me ask this.

Is this a REAL problem YOU have in PRACTICE, or something reported by AI?

I don't have any problem with AI, but I feel like this is solving a problem nobody has

I used DeepSeek as a tool, but the analysis is my own

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants