diff --git a/CLAUDE.md b/CLAUDE.md index 4459bce1e..990bb2e3f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -110,7 +110,7 @@ include/behaviortree_cpp/ ├── controls/ # Control node headers ├── decorators/ # Decorator node headers ├── loggers/ # Logger headers -├── scripting/ # Script parser (lexy-based) +├── scripting/ # Script parser └── contrib/ # Third-party contributions ``` @@ -122,7 +122,7 @@ include/behaviortree_cpp/ ### Vendored Dependencies -All in `3rdparty/`: TinyXML2, cppzmq, flatbuffers, lexy, minicoro, minitrace. Controlled via `USE_VENDORED_*` CMake options. +All in `3rdparty/`: TinyXML2, cppzmq, flatbuffers, minicoro, minitrace. Controlled via `USE_VENDORED_*` CMake options. ## Contributing diff --git a/src/script_tokenizer.cpp b/src/script_tokenizer.cpp index 591f1e4ee..98e3c4174 100644 --- a/src/script_tokenizer.cpp +++ b/src/script_tokenizer.cpp @@ -239,11 +239,11 @@ std::vector tokenize(const std::string& source) const size_t start = i; - // Single-quoted string literal - if(c == '\'') + // String literal: the closing quote must match the opening quote. + if(c == '\'' || c == '"') { ++i; - while(i < len && source[i] != '\'') + while(i < len && source[i] != c) { ++i; } diff --git a/tests/script_parser_test.cpp b/tests/script_parser_test.cpp index 2e2c5559d..64ed0b1c2 100644 --- a/tests/script_parser_test.cpp +++ b/tests/script_parser_test.cpp @@ -85,6 +85,104 @@ TEST(ParserTest, AnyTypes) EXPECT_EQ(result.value().cast(), 0); } +TEST(ParserTest, StringLiteralQuotes) +{ + struct TestCase + { + const char* script; + const char* expected; + }; + const TestCase cases[] = { + { "'hello world '", "hello world " }, + { R"("hello world ")", "hello world " }, + { "''", "" }, + { R"("")", "" }, + { R"('say "hello"')", "say \"hello\"" }, + { R"("it's ready")", "it's ready" }, + { R"('path\to\file')", R"(path\to\file)" }, + { R"("path\to\file")", R"(path\to\file)" }, + { "'first\nsecond'", "first\nsecond" }, + { "\"first\nsecond\"", "first\nsecond" }, + { R"('single'.."double")", "singledouble" }, + }; + BT::Ast::Environment env = { BT::Blackboard::create(), {} }; + for(const auto& test : cases) + { + SCOPED_TRACE(test.script); + auto result = BT::ParseScriptAndExecute(env, test.script); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(result.value().cast(), test.expected); + } + + EXPECT_FALSE(BT::ValidateScript("'unterminated")); + EXPECT_FALSE(BT::ValidateScript(R"("unterminated)")); + EXPECT_FALSE(BT::ValidateScript(R"("mismatched')")); + EXPECT_FALSE(BT::ValidateScript(R"('mismatched")")); +} + +TEST(ParserTest, DoubleQuotedObjectiveExpressions) +{ + BT::Ast::Environment env = { BT::Blackboard::create(), {} }; + env.vars->set("viz_idx", 3); + env.vars->set("index", 2); + env.vars->set("prompt", std::string("bottles")); + env.vars->set("tool_object_name", std::string("tool")); + env.vars->set("execute_trajectories", std::string("true")); + + EXPECT_EQ(GetScriptResult(env, R"(viz_marker_name:="place_pose_"..viz_idx)") + .cast(), + "place_pose_3"); + EXPECT_EQ(GetScriptResult(env, R"(mask_label:="mask "..index)").cast(), + "mask 2"); + EXPECT_EQ( + GetScriptResult(env, R"(header := "SAM3 prompt="..prompt)").cast(), + "SAM3 prompt=bottles"); + EXPECT_EQ(env.vars->get("viz_marker_name"), "place_pose_3"); + EXPECT_EQ(env.vars->get("mask_label"), "mask 2"); + EXPECT_EQ(env.vars->get("header"), "SAM3 prompt=bottles"); + + EXPECT_EQ(GetScriptResult(env, R"(tool_object_name!="")").cast(), 1); + EXPECT_EQ(GetScriptResult(env, R"(execute_trajectories == "true")").cast(), 1); + GetScriptResult(env, R"(tool_object_name=""; execute_trajectories="false")"); + EXPECT_EQ(GetScriptResult(env, R"(tool_object_name!="")").cast(), 0); + EXPECT_EQ(GetScriptResult(env, R"(execute_trajectories == "true")").cast(), 0); +} + +TEST(ParserTest, DoubleQuotedXmlScriptAndPreconditions) +{ + BT::BehaviorTreeFactory factory; + const std::string xml_text = R"( + + + +