Skip to content

Commit 0388ee8

Browse files
store SubTree literals as strings again; coerce numeric strings in script arithmetic (#1246) (#1254)
Since #1121 (shipped in 4.9.1), a numeric literal passed to a SubTree port was stored as int/int64_t/double. That locked the entry type, so the same value could no longer be read by a std::string port (tree creation failed), custom convertFromString<T> was bypassed, and the spelling was lost ("007" became 7). Literals are stored as strings again. The goal of #1121, arithmetic on numeric literals in Script, is kept by the script evaluator instead: in + - * /, a string operand mixed with a number is converted when the whole string is a number (integer first, then double). Behavior change compared to 4.9.1 - 4.10.0, back to pre-4.9.1 behavior: two literal operands are strings ("1" + "2" is "12", "10" < "2" compares text), and compound assignment, unary minus, bitwise and logic operators on such a literal require an explicit number again. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7441d8d commit 0388ee8

4 files changed

Lines changed: 127 additions & 54 deletions

File tree

‎include/behaviortree_cpp/scripting/operators.hpp‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "behaviortree_cpp/scripting/any_types.hpp"
1616
#include "behaviortree_cpp/scripting/script_parser.hpp"
1717

18+
#include <charconv>
1819
#include <cmath>
1920
#include <memory>
2021
#include <string>
@@ -50,6 +51,25 @@ inline double StringToDouble(const Any& value, const Environment& env)
5051
return value.cast<double>();
5152
}
5253

54+
// Replace a string holding a whole number (e.g. "42" or "1.5") with that number.
55+
// Unlike StringToDouble, it rejects numeric prefixes, booleans and enums.
56+
inline void NumericStringToNumber(Any& value)
57+
{
58+
const auto str = value.cast<std::string>();
59+
const char* end = str.data() + str.size();
60+
int64_t int_val = 0;
61+
const auto [ptr, ec] = std::from_chars(str.data(), end, int_val);
62+
double dbl_val = 0;
63+
if(ec == std::errc() && ptr == end)
64+
{
65+
value = Any(int_val);
66+
}
67+
else if(parseDouble(str, dbl_val, /*require_full_consumption=*/true))
68+
{
69+
value = Any(dbl_val);
70+
}
71+
}
72+
5373
struct ExprBase
5474
{
5575
using Ptr = std::shared_ptr<ExprBase>;
@@ -233,6 +253,20 @@ struct ExprBinaryArithmetic : ExprBase
233253
throw RuntimeError(ErrorNotInit("right", opStr()));
234254
}
235255

256+
// A numeric string mixed with a number is used as that number, e.g. the
257+
// literal SubTree port int_val="42" in "int_val + 1"
258+
if(op == plus || op == minus || op == times || op == div)
259+
{
260+
if(lhs_v.isString() && rhs_v.isNumber())
261+
{
262+
NumericStringToNumber(lhs_v);
263+
}
264+
else if(lhs_v.isNumber() && rhs_v.isString())
265+
{
266+
NumericStringToNumber(rhs_v);
267+
}
268+
}
269+
236270
if(rhs_v.isNumber() && lhs_v.isNumber())
237271
{
238272
auto lv = lhs_v.cast<double>();

‎src/xml_parsing.cpp‎

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@
1313
#include "behaviortree_cpp/basic_types.h"
1414

1515
#include <algorithm>
16-
#include <charconv>
1716
#include <cstdio>
1817
#include <cstring>
1918
#include <functional>
2019
#include <iostream>
21-
#include <limits>
2220
#include <list>
2321
#include <sstream>
2422
#include <string>
@@ -1215,58 +1213,12 @@ void BT::XMLParser::PImpl::recursivelyCreateSubtree(
12151213
}
12161214
else
12171215
{
1218-
// constant value: set it into the BB with appropriate type
1216+
// constant string: just set that constant value into the BB.
1217+
// It must stay a string, so that each port converts it to its own type
1218+
// (#1246); Script arithmetic converts numeric strings when needed.
12191219
// IMPORTANT: this must not be autoremapped!!!
12201220
new_bb->enableAutoRemapping(false);
1221-
const std::string str_value(attr_value);
1222-
1223-
// Try to preserve numeric types so that Script expressions
1224-
// can perform arithmetic without type-mismatch errors.
1225-
// Use std::from_chars with strict full-string validation to avoid
1226-
// false positives on compound strings like "1;2;3" or "2.2;2.4".
1227-
bool stored = false;
1228-
if(!str_value.empty())
1229-
{
1230-
const char* begin = str_value.data();
1231-
const char* end = begin + str_value.size();
1232-
// Try integer first (no decimal point, no exponent notation).
1233-
// Use int when the value fits, to match the most common port
1234-
// declarations. Fall back to int64_t for larger values.
1235-
if(str_value.find('.') == std::string::npos &&
1236-
str_value.find('e') == std::string::npos &&
1237-
str_value.find('E') == std::string::npos)
1238-
{
1239-
int64_t int_val = 0;
1240-
auto [ptr, ec] = std::from_chars(begin, end, int_val);
1241-
if(ec == std::errc() && ptr == end)
1242-
{
1243-
if(int_val >= std::numeric_limits<int>::min() &&
1244-
int_val <= std::numeric_limits<int>::max())
1245-
{
1246-
new_bb->set(attr_name, static_cast<int>(int_val));
1247-
}
1248-
else
1249-
{
1250-
new_bb->set(attr_name, int_val);
1251-
}
1252-
stored = true;
1253-
}
1254-
}
1255-
// Try double
1256-
if(!stored)
1257-
{
1258-
double dbl_val = 0;
1259-
if(parseDouble(str_value, dbl_val, /*require_full_consumption=*/true))
1260-
{
1261-
new_bb->set(attr_name, dbl_val);
1262-
stored = true;
1263-
}
1264-
}
1265-
}
1266-
if(!stored)
1267-
{
1268-
new_bb->set(attr_name, str_value);
1269-
}
1221+
new_bb->set(attr_name, static_cast<std::string>(attr_value));
12701222
new_bb->enableAutoRemapping(do_autoremap);
12711223
}
12721224
}

‎tests/gtest_subtree.cpp‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,8 +1017,8 @@ TEST(SubTree, NestedDuplicateNames_ShouldFail)
10171017
ASSERT_THROW((void)factory.createTreeFromText(xml_text), RuntimeError);
10181018
}
10191019

1020-
// Regression test: literal numeric values passed to subtrees should preserve
1021-
// their numeric type so that Script expressions can do arithmetic.
1020+
// Regression test (#1121): Script expressions can do arithmetic with literal
1021+
// numeric values passed to subtrees.
10221022
TEST(SubTree, LiteralNumericPortsPreserveType)
10231023
{
10241024
// clang-format off
@@ -1055,6 +1055,62 @@ TEST(SubTree, LiteralNumericPortsPreserveType)
10551055
ASSERT_EQ(status, NodeStatus::SUCCESS);
10561056
}
10571057

1058+
// Issue #1246: a literal passed to a SubTree stays a string, so ports of different
1059+
// types can read it, and its spelling is preserved.
1060+
TEST(SubTree, LiteralPortsAreReadByAnyPortType)
1061+
{
1062+
// clang-format off
1063+
static const char* xml_text = R"(
1064+
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1065+
1066+
<BehaviorTree ID="MainTree">
1067+
<SubTree ID="Sub" mode="1" code="007" />
1068+
</BehaviorTree>
1069+
1070+
<BehaviorTree ID="Sub">
1071+
<Sequence>
1072+
<ReadString value="{mode}" />
1073+
<ReadInt value="{mode}" />
1074+
<ReadDouble value="{mode}" />
1075+
<ReadString value="{code}" />
1076+
</Sequence>
1077+
</BehaviorTree>
1078+
1079+
</root>
1080+
)";
1081+
// clang-format on
1082+
1083+
BehaviorTreeFactory factory;
1084+
std::vector<std::string> strings;
1085+
int int_val = 0;
1086+
double dbl_val = 0;
1087+
factory.registerSimpleAction("ReadString",
1088+
[&](TreeNode& node) {
1089+
strings.push_back(
1090+
node.getInput<std::string>("value").value());
1091+
return NodeStatus::SUCCESS;
1092+
},
1093+
{ InputPort<std::string>("value") });
1094+
factory.registerSimpleAction("ReadInt",
1095+
[&](TreeNode& node) {
1096+
int_val = node.getInput<int>("value").value();
1097+
return NodeStatus::SUCCESS;
1098+
},
1099+
{ InputPort<int>("value") });
1100+
factory.registerSimpleAction("ReadDouble",
1101+
[&](TreeNode& node) {
1102+
dbl_val = node.getInput<double>("value").value();
1103+
return NodeStatus::SUCCESS;
1104+
},
1105+
{ InputPort<double>("value") });
1106+
1107+
auto tree = factory.createTreeFromText(xml_text);
1108+
ASSERT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
1109+
ASSERT_EQ(strings, (std::vector<std::string>{ "1", "007" }));
1110+
ASSERT_EQ(int_val, 1);
1111+
ASSERT_EQ(dbl_val, 1.0);
1112+
}
1113+
10581114
// Regression test: logical NOT should accept literal boolean values passed to
10591115
// subtrees without changing their string representation in the blackboard.
10601116
TEST(SubTree, LiteralBooleanPortsSupportLogicalNot)

‎tests/script_parser_test.cpp‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,37 @@ TEST(ParserTest, Equations)
270270
EXPECT_EQ(GetResult(" par1:='3'; par2:=4; par1!=par2").cast<int>(), 1);
271271
}
272272

273+
// Issue #1246: a numeric string mixed with a number is used as that number
274+
TEST(ParserTest, NumericStringArithmetic)
275+
{
276+
BT::Ast::Environment environment = { BT::Blackboard::create(), {} };
277+
278+
auto GetResult = [&environment](const char* text) -> BT::Any {
279+
return GetScriptResult(environment, text);
280+
};
281+
environment.vars->set("int_str", std::string("42"));
282+
environment.vars->set("dbl_str", std::string("1.5"));
283+
environment.vars->set("text", std::string("hello"));
284+
285+
EXPECT_EQ(GetResult("int_str + 1").cast<double>(), 43.0);
286+
EXPECT_EQ(GetResult("1 + int_str").cast<double>(), 43.0);
287+
EXPECT_EQ(GetResult("int_str - 2").cast<double>(), 40.0);
288+
EXPECT_EQ(GetResult("int_str * 2").cast<double>(), 84.0);
289+
EXPECT_EQ(GetResult("3 / dbl_str").cast<double>(), 2.0);
290+
EXPECT_EQ(GetResult("'1e3' + 1").cast<double>(), 1001.0);
291+
292+
// the whole string must be a number
293+
EXPECT_ANY_THROW(GetResult("text + 1"));
294+
EXPECT_ANY_THROW(GetResult("'42abc' + 1"));
295+
EXPECT_ANY_THROW(GetResult("'true' + 1"));
296+
EXPECT_ANY_THROW(GetResult("' 42' + 1"));
297+
298+
// unchanged: two strings concatenate, bitwise operators require numbers
299+
EXPECT_EQ(GetResult("int_str + int_str").cast<std::string>(), "4242");
300+
EXPECT_EQ(GetResult("int_str .. 1").cast<std::string>(), "421");
301+
EXPECT_ANY_THROW(GetResult("int_str & 1"));
302+
}
303+
273304
TEST(ParserTest, NotInitializedComparison)
274305
{
275306
BT::Ast::Environment environment = { BT::Blackboard::create(), {} };

0 commit comments

Comments
 (0)