Skip to content

Commit 94bb396

Browse files
committed
Merge branch 'port_tests'
2 parents 5f51577 + 61592fe commit 94bb396

File tree

5 files changed

+1376
-267
lines changed

5 files changed

+1376
-267
lines changed

docs/PORT_CONNECTION_RULES.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# Port Connection and Validation Rules
2+
3+
This document describes the rules that govern how ports can be connected in BehaviorTree.CPP, including type checking, type conversion, and special cases.
4+
5+
## Overview
6+
7+
BehaviorTree.CPP uses a type system for ports that enforces type safety while providing flexibility through several special rules. Type checking occurs primarily at **tree creation time** (when parsing XML), not at runtime.
8+
9+
## Port Types Classification
10+
11+
### 1. Strongly Typed Ports
12+
13+
A port is **strongly typed** when declared with a specific type:
14+
15+
```cpp
16+
InputPort<int>("my_port")
17+
OutputPort<double>("result")
18+
InputPort<Position2D>("goal")
19+
```
20+
21+
### 2. Generic/Weakly Typed Ports (AnyTypeAllowed)
22+
23+
A port is **generic** (not strongly typed) when:
24+
- Declared without a type parameter: `InputPort<>("my_port")`
25+
- Declared with `AnyTypeAllowed`: `InputPort<AnyTypeAllowed>("my_port")`
26+
- Declared with `BT::Any`: `InputPort<BT::Any>("my_port")`
27+
28+
```cpp
29+
// All of these create generic ports:
30+
InputPort<>("value") // defaults to AnyTypeAllowed
31+
InputPort<AnyTypeAllowed>("value") // explicit AnyTypeAllowed
32+
InputPort<BT::Any>("value") // BT::Any type
33+
```
34+
35+
The `isStronglyTyped()` method returns `false` for these ports:
36+
37+
```cpp
38+
// From basic_types.h
39+
bool isStronglyTyped() const
40+
{
41+
return type_info_ != typeid(AnyTypeAllowed) && type_info_ != typeid(BT::Any);
42+
}
43+
```
44+
45+
## Port Connection Rules
46+
47+
### Rule 1: Same Type - Always Compatible
48+
49+
Ports of the **exact same type** can always be connected:
50+
51+
```cpp
52+
// Node A
53+
OutputPort<int>("value") // writes int
54+
55+
// Node B
56+
InputPort<int>("value") // reads int
57+
58+
// Connection: OK
59+
```
60+
61+
**Test reference:** `gtest_port_type_rules.cpp` - `SameType_IntToInt`, `SameType_StringToString`, `SameType_CustomTypeToCustomType` tests
62+
63+
### Rule 2: Generic Port - Compatible with Any Type
64+
65+
A **generic port** (`AnyTypeAllowed` or `BT::Any`) can connect to any other port:
66+
67+
```cpp
68+
// Node A with generic output
69+
OutputPort<>("output") // generic, can write anything
70+
71+
// Node B with typed input
72+
InputPort<int>("input_int") // expects int
73+
74+
// Connection: OK - generic port accepts any type
75+
```
76+
77+
**Test reference:** `gtest_port_type_rules.cpp` - `GenericPort_AcceptsInt`, `GenericPort_AcceptsString`, `GenericOutput_ToTypedInput` tests
78+
79+
### Rule 3: String is a "Universal Donor" (Generic Port)
80+
81+
When a blackboard entry is created as `std::string`, it can be connected to ports of **any type** that has a `convertFromString<T>()` specialization. This is the "string as generic port" rule.
82+
83+
**Source:** `xml_parsing.cpp`
84+
```cpp
85+
// special case related to convertFromString
86+
bool const string_input = (prev_info->type() == typeid(std::string));
87+
88+
if(port_type_mismatch && !string_input)
89+
{
90+
// Error thrown only if NOT a string input
91+
throw RuntimeError("The creation of the tree failed...");
92+
}
93+
```
94+
95+
**Example:**
96+
```xml
97+
<Sequence>
98+
<!-- Creates blackboard entry "value" as string with value "42" -->
99+
<SetBlackboard value="42" output_key="value" />
100+
101+
<!-- Reads "value" as int - OK because string can convert to int -->
102+
<NodeExpectingInt input="{value}" />
103+
</Sequence>
104+
```
105+
106+
**Also applies to:**
107+
- Subtree port passing (string values passed to typed subtree ports)
108+
- Script node assignments
109+
110+
**Test reference:** `gtest_port_type_rules.cpp` - `StringToInt_ViaConvertFromString`, `StringToCustomType_ViaConvertFromString`, `SubtreeStringInput_ToTypedPort` tests
111+
112+
### Rule 4: String Creation in Blackboard
113+
114+
When using `Blackboard::set<std::string>()`, the entry is created with `AnyTypeAllowed` type, not `std::string`:
115+
116+
**Source:** `blackboard.h`
117+
```cpp
118+
// if a new generic port is created with a string, it's type should be AnyTypeAllowed
119+
if constexpr(std::is_same_v<std::string, T>)
120+
{
121+
entry = createEntryImpl(key, PortInfo(PortDirection::INOUT)); // AnyTypeAllowed
122+
}
123+
```
124+
125+
This allows subsequent writes of different types to the same entry.
126+
127+
**Test reference:** `gtest_port_type_rules.cpp` - `BlackboardSetString_CreatesGenericEntry`, `StringEntry_CanBecomeTyped` tests
128+
129+
### Rule 5: Type Lock After First Strongly-Typed Write
130+
131+
Once a blackboard entry receives a **strongly typed** value, its type is locked:
132+
133+
**Source:** `blackboard.h`
134+
```cpp
135+
// special case: entry exists but it is not strongly typed... yet
136+
if(!entry.info.isStronglyTyped())
137+
{
138+
// Use the new type to create a strongly typed entry
139+
entry.info = TypeInfo::Create<T>();
140+
// ...
141+
return;
142+
}
143+
```
144+
145+
After this, writing a different type will fail (with exceptions noted below).
146+
147+
**Test reference:** `gtest_port_type_rules.cpp` - `TypeLock_CannotChangeAfterTypedWrite`, `TypeLock_XMLTreeCreation_TypeMismatch`, `TypeLock_RuntimeTypeChange_Fails` tests
148+
149+
### Rule 6: BT::Any Bypasses Type Checking
150+
151+
When a blackboard entry is **created with type `BT::Any`**, it can store different types over time. This requires the entry to be explicitly created as `BT::Any` type.
152+
153+
**Important:** Wrapping a value with `BT::Any()` does **not** bypass type checking - the wrapper is unwrapped and the inner type is used:
154+
155+
```cpp
156+
// This creates an entry of type int, NOT BT::Any
157+
bb->set("key", BT::Any(42));
158+
159+
// This will FAIL - entry is int, not BT::Any
160+
bb->set("key", BT::Any("hello")); // throws LogicError
161+
```
162+
163+
To actually allow different types, create the entry as `BT::Any`:
164+
165+
```cpp
166+
// Create entry explicitly as BT::Any type
167+
bb->createEntry("key", TypeInfo::Create<BT::Any>());
168+
169+
// Now different types are allowed
170+
bb->set("key", BT::Any(42)); // OK
171+
bb->set("key", BT::Any("hello")); // OK
172+
bb->set("key", BT::Any(3.14)); // OK
173+
```
174+
175+
**Test reference:** `gtest_port_type_rules.cpp` - `BTAny_WrapperDoesNotBypassTypeCheck`, `BTAny_EntryType_AllowsDifferentTypes`, `BTAny_Port_AcceptsDifferentTypes` tests
176+
177+
### Rule 7: Type Mismatch Between Strongly Typed Ports - Error
178+
179+
If two **strongly typed** ports with **different types** try to use the same blackboard entry, an error is thrown at tree creation:
180+
181+
```xml
182+
<!-- This will FAIL at tree creation -->
183+
<Sequence>
184+
<NodeA output_int="{value}" /> <!-- Creates entry as int -->
185+
<NodeB input_string="{value}" /> <!-- Tries to read as string - ERROR -->
186+
</Sequence>
187+
```
188+
189+
**Test reference:** `gtest_port_type_rules.cpp` - `TypeLock_XMLTreeCreation_TypeMismatch`, `TypeLock_IntToDouble_Fails`, `TypeLock_CustomTypeChange_Fails` tests
190+
191+
## Type Conversion via convertFromString
192+
193+
### Built-in Conversions
194+
195+
The library provides `convertFromString<T>()` for:
196+
- `int`, `long`, `long long`, and unsigned variants
197+
- `float`, `double`
198+
- `bool` (accepts "true"/"false", "1"/"0")
199+
- `std::string`
200+
- `std::vector<T>` (semicolon-separated values)
201+
- Enums (when registered)
202+
203+
### Custom Type Conversion
204+
205+
To make a custom type compatible with string ports, specialize `convertFromString`:
206+
207+
```cpp
208+
namespace BT
209+
{
210+
template <>
211+
inline Position2D convertFromString(StringView str)
212+
{
213+
auto parts = splitString(str, ';');
214+
if(parts.size() != 2)
215+
throw RuntimeError("invalid input");
216+
217+
Position2D output;
218+
output.x = convertFromString<double>(parts[0]);
219+
output.y = convertFromString<double>(parts[1]);
220+
return output;
221+
}
222+
}
223+
```
224+
225+
**Test reference:** `gtest_ports.cpp`, `t03_generic_ports.cpp`
226+
227+
### JSON Format Support
228+
229+
Custom types can also use JSON format with "json:" prefix:
230+
231+
```cpp
232+
InputPort<Point2D>("pointE", R"(json:{"x":9,"y":10})", "description")
233+
```
234+
235+
**Test reference:** `gtest_ports.cpp`
236+
237+
## Validation Timeline
238+
239+
### At Tree Creation (XML Parsing)
240+
241+
1. **Port name validation** - Checks port exists in node manifest
242+
2. **Literal value validation** - If port value is not a blackboard reference, validates conversion
243+
3. **Blackboard entry type check** - If entry exists, checks type compatibility
244+
245+
**Source:** `xml_parsing.cpp`
246+
```cpp
247+
if(!is_blackboard && port_model.converter() && port_model.isStronglyTyped())
248+
{
249+
try
250+
{
251+
port_model.converter()(port_value); // Validate conversion
252+
}
253+
catch(std::exception& ex)
254+
{
255+
throw LogicError("The port... can not be converted to " + port_model.typeName());
256+
}
257+
}
258+
```
259+
260+
### At Runtime (Blackboard::set)
261+
262+
1. **Type match check** - Compares new type with entry's declared type
263+
2. **String conversion attempt** - If mismatch, tries `parseString()`
264+
265+
## Summary Table
266+
267+
| Scenario | Compatible? | Notes |
268+
|----------|-------------|-------|
269+
| Same types | Yes | Always works |
270+
| Generic port (either side) | Yes | `AnyTypeAllowed` or `BT::Any` |
271+
| String → Typed port | Yes | Via `convertFromString<T>()` |
272+
| Typed → String port | No | Type mismatch error |
273+
| int → double | No | Different strongly-typed |
274+
| Point2D → std::string | No | Unless entry was string first |
275+
| BT::Any entry to anything | Yes | Entry must be created as BT::Any type |
276+
277+
## Reserved Port Names
278+
279+
The following names **cannot** be used for ports:
280+
- `name` - Reserved for node instance name
281+
- `ID` - Reserved for node type ID
282+
- Names starting with `_` - Reserved for internal use
283+
284+
**Test reference:** `gtest_port_type_rules.cpp` - `ReservedPortName_ThrowsOnRegistration` test
285+
286+
## Common Patterns
287+
288+
### Pattern 1: Type-Safe Port Chain
289+
```xml
290+
<Sequence>
291+
<CalculateGoal goal="{GoalPosition}" /> <!-- Output: Position2D -->
292+
<PrintTarget target="{GoalPosition}" /> <!-- Input: Position2D -->
293+
</Sequence>
294+
```
295+
296+
### Pattern 2: String Literal to Typed Port
297+
```xml
298+
<Sequence>
299+
<SetBlackboard value="1.5;2.5" output_key="pos" />
300+
<MoveToPosition target="{pos}" /> <!-- Converts string to Position2D -->
301+
</Sequence>
302+
```
303+
304+
### Pattern 3: Generic Intermediate Storage
305+
```xml
306+
<Sequence>
307+
<TypedNode output_int="{value}" /> <!-- Writes int to generic entry -->
308+
<GenericNode input="{value}" /> <!-- Generic port reads it -->
309+
<AnotherTypedNode input_int="{value}"/> <!-- Int port reads it -->
310+
</Sequence>
311+
```
312+
313+
## Error Messages
314+
315+
Common type-related errors:
316+
317+
1. **"The creation of the tree failed because the port [X] was initially created with type [A] and, later type [B] was used somewhere else."**
318+
- Cause: Two nodes use same blackboard key with incompatible types
319+
- Solution: Ensure consistent types or use string/generic ports
320+
321+
2. **"Blackboard::set(X): once declared, the type of a port shall not change."**
322+
- Cause: Runtime attempt to change entry type
323+
- Solution: Use consistent types or BT::Any
324+
325+
3. **"The port with name X and value Y can not be converted to Z"**
326+
- Cause: Literal value cannot be parsed to port type
327+
- Solution: Fix value format or add `convertFromString` specialization
328+
329+
## References
330+
331+
- Source: `include/behaviortree_cpp/basic_types.h` - Type system definitions
332+
- Source: `include/behaviortree_cpp/blackboard.h` - Blackboard type checking
333+
- Source: `src/xml_parsing.cpp` - Tree creation validation
334+
- Tests: `tests/gtest_port_type_rules.cpp` - Comprehensive port type rule tests
335+
- Tests: `tests/gtest_ports.cpp` - Port connection tests
336+
- Tests: `tests/gtest_blackboard.cpp` - Blackboard tests
337+
- Tutorial: `examples/t03_generic_ports.cpp` - Custom type example

tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ set(BT_TESTS
1515
gtest_parallel.cpp
1616
gtest_preconditions.cpp
1717
gtest_ports.cpp
18+
gtest_port_type_rules.cpp
1819
gtest_postconditions.cpp
1920
gtest_match.cpp
2021
gtest_json.cpp

0 commit comments

Comments
 (0)