Skip to content

Commit 183a390

Browse files
authored
Merge pull request #46 from PickNikRobotics/revert/45-script-numeric-strings
Revert script numeric-string sync (#45) until 11.0
2 parents 4633186 + 913cedc commit 183a390

6 files changed

Lines changed: 12 additions & 342 deletions

File tree

‎include/behaviortree_cpp/scripting/operators.hpp‎

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

18-
#include <cctype>
19-
#include <charconv>
2018
#include <cmath>
21-
#include <locale>
2219
#include <memory>
23-
#include <sstream>
2420
#include <string>
2521
#include <vector>
2622

@@ -54,39 +50,6 @@ inline double StringToDouble(const Any& value, const Environment& env)
5450
return value.cast<double>();
5551
}
5652

57-
// Replace a string holding a whole number (e.g. "42" or "1.5") with that number.
58-
// Unlike StringToDouble, it rejects numeric prefixes, booleans and enums.
59-
inline void NumericStringToNumber(Any& value)
60-
{
61-
const auto str = value.cast<std::string>();
62-
const char* end = str.data() + str.size();
63-
int64_t int_val = 0;
64-
const auto [ptr, ec] = std::from_chars(str.data(), end, int_val);
65-
double dbl_val = 0;
66-
if(ec == std::errc() && ptr == end)
67-
{
68-
value = Any(int_val);
69-
return;
70-
}
71-
// Upstream calls parseDouble(str, dbl_val, true), which its #1167 added and this fork doesn't
72-
// have yet: from_chars with a full-consumption check, or a classic-locale stream without it.
73-
#if __cpp_lib_to_chars >= 201611L
74-
const auto [dbl_ptr, dbl_ec] = std::from_chars(str.data(), end, dbl_val);
75-
const bool parsed = dbl_ec == std::errc() && dbl_ptr == end;
76-
#else
77-
std::istringstream stream(str);
78-
stream.imbue(std::locale::classic());
79-
stream >> dbl_val;
80-
const bool parsed = !str.empty() &&
81-
!std::isspace(static_cast<unsigned char>(str.front())) &&
82-
!stream.fail() && stream.peek() == std::char_traits<char>::eof();
83-
#endif
84-
if(parsed)
85-
{
86-
value = Any(dbl_val);
87-
}
88-
}
89-
9053
struct ExprBase
9154
{
9255
using Ptr = std::shared_ptr<ExprBase>;
@@ -166,38 +129,18 @@ struct ExprUnaryArithmetic : ExprBase
166129
case negate:
167130
return Any(-rv);
168131
case complement:
169-
// Use the checked conversion, like the binary bitwise operators do.
170-
// A plain static_cast is undefined for NaN and for values at or
171-
// beyond the int64_t range: the hand-written comparison used here
172-
// before let NaN (every comparison is false) and 2^63 (not greater
173-
// than double(INT64_MAX), which rounds up to 2^63) through, and
174-
// silently truncated fractional values.
175-
try
176-
{
177-
return Any(static_cast<double>(~rhs_v.cast<int64_t>()));
178-
}
179-
catch(const std::exception&)
132+
if(rv > static_cast<double>(std::numeric_limits<int64_t>::max()) ||
133+
rv < static_cast<double>(std::numeric_limits<int64_t>::min()))
180134
{
181135
throw RuntimeError("Number out of range for bitwise operation");
182136
}
137+
return Any(static_cast<double>(~static_cast<int64_t>(rv)));
183138
case logical_not:
184139
return Any(static_cast<double>(!static_cast<bool>(rv)));
185140
}
186141
}
187142
else if(rhs_v.isString())
188143
{
189-
if(op == logical_not)
190-
{
191-
const auto str = rhs_v.cast<std::string>();
192-
if(str == "true" || str == "True" || str == "TRUE" || str == "1")
193-
{
194-
return Any(0.0);
195-
}
196-
if(str == "false" || str == "False" || str == "FALSE" || str == "0")
197-
{
198-
return Any(1.0);
199-
}
200-
}
201144
throw RuntimeError("Invalid operator for std::string");
202145
}
203146
throw RuntimeError("ExprUnaryArithmetic: undefined");
@@ -270,21 +213,11 @@ struct ExprBinaryArithmetic : ExprBase
270213
throw RuntimeError(ErrorNotInit("right", opStr()));
271214
}
272215

273-
// A numeric string mixed with a number is used as that number, e.g. the
274-
// literal SubTree port int_val="42" in "int_val + 1"
275-
if(op == plus || op == minus || op == times || op == div)
276-
{
277-
if(lhs_v.isString() && rhs_v.isNumber())
278-
{
279-
NumericStringToNumber(lhs_v);
280-
}
281-
else if(lhs_v.isNumber() && rhs_v.isString())
282-
{
283-
NumericStringToNumber(rhs_v);
284-
}
285-
}
286-
287-
if(rhs_v.isNumber() && lhs_v.isNumber())
216+
// If just one value is strongly typed as a number, and the other can be casted to one, do arithmetic. If both are not strongly typed, continue. In the case of `+`, non-strongly typed values will result in concatenation.
217+
bool are_numbers = (rhs_v.isNumber() && lhs_v.isNumber()) ||
218+
((rhs_v.tryCast<double>().has_value() && lhs_v.isNumber()) !=
219+
(rhs_v.isNumber() && lhs_v.tryCast<double>().has_value()));
220+
if(are_numbers)
288221
{
289222
auto lv = lhs_v.cast<double>();
290223
auto rv = rhs_v.cast<double>();

‎include/behaviortree_cpp/utils/convert_impl.hpp‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,15 +114,8 @@ inline void checkTruncation(const From& from)
114114
// Handle floating point to integer
115115
else if constexpr(std::is_floating_point_v<From> && std::is_integral_v<To>)
116116
{
117-
// static_cast<From>(To::max) rounds up to an out-of-range value when To has
118-
// more significant bits than From's mantissa (e.g. double -> int64_t). In
119-
// that case a plain `>` lets the rounded boundary through and the cast to To
120-
// below overflows, so use a half-open upper bound to reject it first.
121-
constexpr bool max_rounds_up =
122-
std::numeric_limits<To>::digits > std::numeric_limits<From>::digits;
123-
const From max_limit = static_cast<From>(std::numeric_limits<To>::max());
124-
const bool over = max_rounds_up ? (from >= max_limit) : (from > max_limit);
125-
if(over || from < static_cast<From>(std::numeric_limits<To>::lowest()) ||
117+
if(from > static_cast<From>(std::numeric_limits<To>::max()) ||
118+
from < static_cast<From>(std::numeric_limits<To>::lowest()) ||
126119
from != std::nearbyint(from))
127120
{
128121
throw std::runtime_error("Invalid floating point to integer conversion");

‎include/behaviortree_cpp/utils/safe_any.hpp‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -259,16 +259,8 @@ inline bool ValidCast(const SRC& val)
259259
// Handle conversion to integral
260260
else if constexpr(std::is_integral_v<TO>)
261261
{
262-
// static_cast<SRC>(TO::max) rounds up to an out-of-range value when TO has
263-
// more significant bits than a floating SRC's mantissa (e.g. double ->
264-
// int64_t). A plain `>` would let that rounded boundary through and the
265-
// cast to TO below overflows, so use a half-open upper bound in that case.
266-
constexpr bool max_rounds_up =
267-
std::is_floating_point_v<SRC> &&
268-
std::numeric_limits<TO>::digits > std::numeric_limits<SRC>::digits;
269-
const SRC max_limit = static_cast<SRC>(std::numeric_limits<TO>::max());
270-
const bool over = max_rounds_up ? (val >= max_limit) : (val > max_limit);
271-
if(over || val < static_cast<SRC>(std::numeric_limits<TO>::lowest()))
262+
if(val > static_cast<SRC>(std::numeric_limits<TO>::max()) ||
263+
val < static_cast<SRC>(std::numeric_limits<TO>::lowest()))
272264
{
273265
return false;
274266
}

‎tests/gtest_basic_types.cpp‎

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -123,31 +123,6 @@ TEST(BasicTypes, ConvertFromString_Double)
123123
ASSERT_THROW((void)convertFromString<double>("not_a_number"), RuntimeError);
124124
}
125125

126-
TEST(BasicTypes, DoubleToIntBoundary)
127-
{
128-
// A double that lands exactly on 2^63 must be rejected before the cast, not
129-
// static_cast to int64_t (which is out of range and undefined behavior).
130-
// 2^63 = 9223372036854775808.0; INT64_MAX is 9223372036854775807.
131-
const double two_pow_63 = 9223372036854775808.0;
132-
EXPECT_ANY_THROW((void)BT::Any(two_pow_63).cast<int64_t>());
133-
134-
// 2^64 lands one past UINT64_MAX and must likewise be rejected.
135-
const double two_pow_64 = 18446744073709551616.0;
136-
EXPECT_ANY_THROW((void)BT::Any(two_pow_64).cast<uint64_t>());
137-
138-
// In-range integral doubles still convert cleanly.
139-
const double in_range = 4611686018427387904.0; // 2^62
140-
EXPECT_EQ(BT::Any(in_range).cast<int64_t>(), int64_t(4611686018427387904LL));
141-
EXPECT_EQ(BT::Any(1000.0).cast<int64_t>(), int64_t(1000));
142-
143-
// Same boundary reached through Blackboard::set, which routes the numeric
144-
// safe-cast check (isCastingSafe/ValidCast) instead of Any::convert.
145-
auto bb = BT::Blackboard::create();
146-
bb->set("k", int64_t(1)); // lock the entry to int64_t
147-
EXPECT_ANY_THROW(bb->set("k", two_pow_63));
148-
EXPECT_NO_THROW(bb->set("k", in_range));
149-
}
150-
151126
TEST(BasicTypes, ConvertFromString_Bool)
152127
{
153128
ASSERT_TRUE(convertFromString<bool>("true"));

‎tests/gtest_subtree.cpp‎

Lines changed: 0 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,175 +1089,3 @@ TEST(SubTree, NoNameAttribute_AutoGeneratesUniquePaths)
10891089
auto status = tree.tickWhileRunning();
10901090
ASSERT_EQ(status, NodeStatus::SUCCESS);
10911091
}
1092-
1093-
// Regression test: logical NOT should accept literal boolean values passed to
1094-
// subtrees without changing their string representation in the blackboard.
1095-
TEST(SubTree, LiteralBooleanPortsSupportLogicalNot)
1096-
{
1097-
// clang-format off
1098-
static const char* xml_text = R"(
1099-
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1100-
1101-
<BehaviorTree ID="MainTree">
1102-
<SubTree ID="CheckBooleans" enabled="true" disabled="false"
1103-
enabled_upper="TRUE" disabled_title="False"
1104-
enabled_numeric="{enabled_numeric}"
1105-
disabled_numeric="{disabled_numeric}" />
1106-
</BehaviorTree>
1107-
1108-
<BehaviorTree ID="CheckBooleans">
1109-
<Sequence>
1110-
<AlwaysSuccess _skipIf="!enabled" />
1111-
<ScriptCondition code="!disabled" />
1112-
<ScriptCondition code="!!enabled_upper" />
1113-
<ScriptCondition code="!disabled_title" />
1114-
<ScriptCondition code="!!enabled_numeric" />
1115-
<ScriptCondition code="!disabled_numeric" />
1116-
</Sequence>
1117-
</BehaviorTree>
1118-
1119-
<TreeNodesModel>
1120-
<SubTree ID="CheckBooleans">
1121-
<input_port name="enabled" type="bool" />
1122-
<input_port name="disabled" type="bool" />
1123-
<input_port name="enabled_upper" type="bool" />
1124-
<input_port name="disabled_title" type="bool" />
1125-
<input_port name="enabled_numeric" type="bool" />
1126-
<input_port name="disabled_numeric" type="bool" />
1127-
</SubTree>
1128-
</TreeNodesModel>
1129-
1130-
</root>
1131-
)";
1132-
// clang-format on
1133-
1134-
BehaviorTreeFactory factory;
1135-
auto tree = factory.createTreeFromText(xml_text);
1136-
tree.rootBlackboard()->set("enabled_numeric", std::string("1"));
1137-
tree.rootBlackboard()->set("disabled_numeric", std::string("0"));
1138-
1139-
ASSERT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
1140-
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("enabled"), "true");
1141-
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("disabled"), "false");
1142-
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("enabled_upper"), "TRUE");
1143-
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("disabled_title"), "False");
1144-
}
1145-
1146-
TEST(SubTree, LiteralNonBooleanPortsRejectLogicalNot)
1147-
{
1148-
// clang-format off
1149-
static const char* xml_text = R"(
1150-
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1151-
1152-
<BehaviorTree ID="MainTree">
1153-
<SubTree ID="CheckValue" value="1not_bool" />
1154-
</BehaviorTree>
1155-
1156-
<BehaviorTree ID="CheckValue">
1157-
<ScriptCondition code="!value" />
1158-
</BehaviorTree>
1159-
1160-
</root>
1161-
)";
1162-
// clang-format on
1163-
1164-
BehaviorTreeFactory factory;
1165-
auto tree = factory.createTreeFromText(xml_text);
1166-
1167-
ASSERT_THROW((void)tree.tickWhileRunning(), RuntimeError);
1168-
ASSERT_EQ(tree.subtrees[1]->blackboard->get<std::string>("value"), "1not_bool");
1169-
}
1170-
1171-
// Regression test (#1121): Script expressions can do arithmetic with literal
1172-
// numeric values passed to subtrees.
1173-
TEST(SubTree, LiteralNumericPortsPreserveType)
1174-
{
1175-
// clang-format off
1176-
static const char* xml_text = R"(
1177-
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1178-
1179-
<BehaviorTree ID="MainTree">
1180-
<Sequence>
1181-
<SubTree ID="DoMath" int_val="42" dbl_val="3.14" str_val="hello"
1182-
remapped_val="{from_parent}" />
1183-
</Sequence>
1184-
</BehaviorTree>
1185-
1186-
<BehaviorTree ID="DoMath">
1187-
<Sequence>
1188-
<ScriptCondition code=" int_val + 1 == 43 " />
1189-
<ScriptCondition code=" dbl_val > 3.0 " />
1190-
<ScriptCondition code=" remapped_val + 1 == 101 " />
1191-
</Sequence>
1192-
</BehaviorTree>
1193-
1194-
</root>
1195-
)";
1196-
// clang-format on
1197-
1198-
BehaviorTreeFactory factory;
1199-
1200-
auto tree = factory.createTreeFromText(xml_text);
1201-
1202-
// Set the remapped parent value as an integer
1203-
tree.rootBlackboard()->set("from_parent", 100);
1204-
1205-
const auto status = tree.tickWhileRunning();
1206-
ASSERT_EQ(status, NodeStatus::SUCCESS);
1207-
}
1208-
1209-
// Issue #1246: a literal passed to a SubTree stays a string, so ports of different
1210-
// types can read it, and its spelling is preserved.
1211-
TEST(SubTree, LiteralPortsAreReadByAnyPortType)
1212-
{
1213-
// clang-format off
1214-
static const char* xml_text = R"(
1215-
<root BTCPP_format="4" main_tree_to_execute="MainTree">
1216-
1217-
<BehaviorTree ID="MainTree">
1218-
<SubTree ID="Sub" mode="1" code="007" />
1219-
</BehaviorTree>
1220-
1221-
<BehaviorTree ID="Sub">
1222-
<Sequence>
1223-
<ReadString value="{mode}" />
1224-
<ReadInt value="{mode}" />
1225-
<ReadDouble value="{mode}" />
1226-
<ReadString value="{code}" />
1227-
</Sequence>
1228-
</BehaviorTree>
1229-
1230-
</root>
1231-
)";
1232-
// clang-format on
1233-
1234-
BehaviorTreeFactory factory;
1235-
std::vector<std::string> strings;
1236-
int int_val = 0;
1237-
double dbl_val = 0;
1238-
factory.registerSimpleAction("ReadString",
1239-
[&](TreeNode& node) {
1240-
strings.push_back(
1241-
node.getInput<std::string>("value").value());
1242-
return NodeStatus::SUCCESS;
1243-
},
1244-
{ InputPort<std::string>("value") });
1245-
factory.registerSimpleAction("ReadInt",
1246-
[&](TreeNode& node) {
1247-
int_val = node.getInput<int>("value").value();
1248-
return NodeStatus::SUCCESS;
1249-
},
1250-
{ InputPort<int>("value") });
1251-
factory.registerSimpleAction("ReadDouble",
1252-
[&](TreeNode& node) {
1253-
dbl_val = node.getInput<double>("value").value();
1254-
return NodeStatus::SUCCESS;
1255-
},
1256-
{ InputPort<double>("value") });
1257-
1258-
auto tree = factory.createTreeFromText(xml_text);
1259-
ASSERT_EQ(tree.tickWhileRunning(), NodeStatus::SUCCESS);
1260-
ASSERT_EQ(strings, (std::vector<std::string>{ "1", "007" }));
1261-
ASSERT_EQ(int_val, 1);
1262-
ASSERT_EQ(dbl_val, 1.0);
1263-
}

0 commit comments

Comments
 (0)