Skip to content

Commit 0125875

Browse files
Detect recursive subtree cycles at parse time (fixes #979) (#1085)
* Detect recursive subtree cycles at parse time (fixes #979) Use an ancestor set passed through recursivelyCreateSubtree to detect cycles, avoiding the substring-matching false positives of a prefix path check. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Pass ancestors set by reference instead of by value Avoids copying the set at each recursive call. Uses insert-on-entry and erase-on-exit (DFS backtracking) so sibling subtrees sharing the same ID are handled correctly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5060cd4 commit 0125875

File tree

2 files changed

+96
-9
lines changed

2 files changed

+96
-9
lines changed

src/xml_parsing.cpp

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <string>
2222
#include <tuple>
2323
#include <typeindex>
24+
#include <unordered_set>
2425

2526
#if defined(_MSVC_LANG) && !defined(__clang__)
2627
#define __bt_cplusplus (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
@@ -196,7 +197,8 @@ struct XMLParser::PImpl
196197
void recursivelyCreateSubtree(const std::string& tree_ID, const std::string& tree_path,
197198
const std::string& prefix_path, Tree& output_tree,
198199
Blackboard::Ptr blackboard,
199-
const TreeNode::Ptr& root_node);
200+
const TreeNode::Ptr& root_node,
201+
std::unordered_set<std::string>& ancestors);
200202

201203
void getPortsRecursively(const XMLElement* element,
202204
std::vector<std::string>& output_ports);
@@ -703,8 +705,9 @@ Tree XMLParser::instantiateTree(const Blackboard::Ptr& root_blackboard,
703705
"root_blackboard");
704706
}
705707

708+
std::unordered_set<std::string> ancestors;
706709
_p->recursivelyCreateSubtree(main_tree_ID, {}, {}, output_tree, root_blackboard,
707-
TreeNode::Ptr());
710+
TreeNode::Ptr(), ancestors);
708711
output_tree.initialize();
709712
return output_tree;
710713
}
@@ -1005,13 +1008,16 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element,
10051008
return new_node;
10061009
}
10071010

1008-
void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
1009-
const std::string& tree_path,
1010-
const std::string& prefix_path,
1011-
Tree& output_tree,
1012-
Blackboard::Ptr blackboard,
1013-
const TreeNode::Ptr& root_node)
1011+
void BT::XMLParser::PImpl::recursivelyCreateSubtree(
1012+
const std::string& tree_ID, const std::string& tree_path,
1013+
const std::string& prefix_path, Tree& output_tree, Blackboard::Ptr blackboard,
1014+
const TreeNode::Ptr& root_node, std::unordered_set<std::string>& ancestors)
10141015
{
1016+
if(!ancestors.insert(tree_ID).second)
1017+
{
1018+
throw RuntimeError("Recursive behavior tree cycle detected: tree '", tree_ID,
1019+
"' references itself (directly or indirectly)");
1020+
}
10151021
std::function<void(const TreeNode::Ptr&, Tree::Subtree::Ptr, std::string,
10161022
const XMLElement*)>
10171023
recursiveStep;
@@ -1140,7 +1146,7 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
11401146
recursivelyCreateSubtree(subtree_ID,
11411147
subtree_path, // name
11421148
subtree_path + "/", //prefix
1143-
output_tree, new_bb, node);
1149+
output_tree, new_bb, node, ancestors);
11441150
}
11451151
};
11461152

@@ -1162,6 +1168,7 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(const std::string& tree_ID,
11621168
output_tree.subtrees.push_back(new_tree);
11631169

11641170
recursiveStep(root_node, new_tree, prefix_path, root_element);
1171+
ancestors.erase(tree_ID);
11651172
}
11661173

11671174
void XMLParser::PImpl::getPortsRecursively(const XMLElement* element,

tests/gtest_subtree.cpp

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,86 @@ TEST(SubTree, SubtreeNameNotRegistered)
732732
ASSERT_ANY_THROW(factory.registerBehaviorTreeFromText(xml_text));
733733
}
734734

735+
TEST(SubTree, RecursiveSubtree)
736+
{
737+
// clang-format off
738+
739+
static const char* xml_text = R"(
740+
<root BTCPP_format="4" >
741+
<BehaviorTree ID="MainTree">
742+
<Sequence name="root">
743+
<AlwaysSuccess/>
744+
<SubTree ID="MainTree" />
745+
</Sequence>
746+
</BehaviorTree>
747+
</root>
748+
)";
749+
750+
// clang-format on
751+
BehaviorTreeFactory factory;
752+
753+
ASSERT_ANY_THROW(auto tree = factory.createTreeFromText(xml_text));
754+
}
755+
756+
TEST(SubTree, RecursiveCycle)
757+
{
758+
// clang-format off
759+
760+
static const char* xml_text = R"(
761+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
762+
<BehaviorTree ID="MainTree">
763+
<Sequence name="root">
764+
<AlwaysSuccess/>
765+
<SubTree ID="TreeA" />
766+
</Sequence>
767+
</BehaviorTree>
768+
769+
<BehaviorTree ID="TreeA">
770+
<Sequence name="root">
771+
<AlwaysSuccess/>
772+
<SubTree ID="TreeB" />
773+
</Sequence>
774+
</BehaviorTree>
775+
776+
<BehaviorTree ID="TreeB">
777+
<Sequence name="root">
778+
<AlwaysSuccess/>
779+
<SubTree ID="MainTree" />
780+
</Sequence>
781+
</BehaviorTree>
782+
</root>
783+
)";
784+
785+
// clang-format on
786+
BehaviorTreeFactory factory;
787+
788+
ASSERT_ANY_THROW(auto tree = factory.createTreeFromText(xml_text));
789+
}
790+
791+
TEST(SubTree, SubstringTreeIDsAreNotRecursive)
792+
{
793+
// Verify that tree IDs which are substrings of each other do NOT
794+
// incorrectly trigger the recursive cycle detection.
795+
// clang-format off
796+
797+
static const char* xml_text = R"(
798+
<root BTCPP_format="4" main_tree_to_execute="Tree">
799+
<BehaviorTree ID="Tree">
800+
<SubTree ID="TreeABC" />
801+
</BehaviorTree>
802+
803+
<BehaviorTree ID="TreeABC">
804+
<AlwaysSuccess/>
805+
</BehaviorTree>
806+
</root>
807+
)";
808+
809+
// clang-format on
810+
BehaviorTreeFactory factory;
811+
812+
ASSERT_NO_THROW(auto tree = factory.createTreeFromText(xml_text));
813+
}
814+
735815
// Test for Groot2 issue #56: duplicate _fullpath when multiple subtrees have the same name
736816
// https://github.com/BehaviorTree/Groot2/issues/56
737817
//

0 commit comments

Comments
 (0)