From d56a2792ec35f9599296be6c95c18bc2e64bbed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Del=20Pino?= <stephane.delpino44@gmail.com> Date: Sat, 21 May 2022 12:11:12 +0200 Subject: [PATCH] Change behavior of string concatenation binary operator For (language) consistency, now concatenation binary operators allow the string to be the right hand side. For instance B + string -> string it was not accepted before and produced a compilation error --- .../ConcatExpressionProcessor.hpp | 25 +- .../utils/BinaryOperatorRegisterForString.cpp | 8 +- .../ConcatExpressionProcessorBuilder.hpp | 9 +- ...ASTNodeBinaryOperatorExpressionBuilder.cpp | 341 +++++++++++++++++- tests/test_ASTNodeDataTypeBuilder.cpp | 13 - tests/test_ConcatExpressionProcessor.cpp | 96 +++++ 6 files changed, 456 insertions(+), 36 deletions(-) diff --git a/src/language/node_processor/ConcatExpressionProcessor.hpp b/src/language/node_processor/ConcatExpressionProcessor.hpp index 718325641..43b785979 100644 --- a/src/language/node_processor/ConcatExpressionProcessor.hpp +++ b/src/language/node_processor/ConcatExpressionProcessor.hpp @@ -4,7 +4,7 @@ #include <language/ast/ASTNode.hpp> #include <language/node_processor/INodeProcessor.hpp> -template <typename B_DataT> +template <typename A_DataT, typename B_DataT> class ConcatExpressionProcessor final : public INodeProcessor { private: @@ -25,12 +25,31 @@ class ConcatExpressionProcessor final : public INodeProcessor } } + PUGS_INLINE + DataVariant + _eval(const DataVariant& a, const std::string& b) + { + static_assert(not std::is_same_v<A_DataT, std::string>, "this case is treated by the other eval function"); + if constexpr ((std::is_arithmetic_v<A_DataT>)and(not std::is_same_v<A_DataT, bool>)) { + return std::to_string(std::get<A_DataT>(a)) + b; + } else { + std::ostringstream os; + os << std::boolalpha << a << b; + return os.str(); + } + } + public: DataVariant execute(ExecutionPolicy& exec_policy) { - return this->_eval(std::get<std::string>(m_node.children[0]->execute(exec_policy)), - m_node.children[1]->execute(exec_policy)); + if constexpr (std::is_same_v<std::string, A_DataT>) { + return this->_eval(std::get<std::string>(m_node.children[0]->execute(exec_policy)), + m_node.children[1]->execute(exec_policy)); + } else { + return this->_eval(m_node.children[0]->execute(exec_policy), + std::get<std::string>(m_node.children[1]->execute(exec_policy))); + } } ConcatExpressionProcessor(ASTNode& node) : m_node{node} {} diff --git a/src/language/utils/BinaryOperatorRegisterForString.cpp b/src/language/utils/BinaryOperatorRegisterForString.cpp index cae341bf9..e16c884ed 100644 --- a/src/language/utils/BinaryOperatorRegisterForString.cpp +++ b/src/language/utils/BinaryOperatorRegisterForString.cpp @@ -32,13 +32,17 @@ BinaryOperatorRegisterForString::_register_comparisons() std::make_shared<BinaryOperatorProcessorBuilder<language::not_eq_op, bool, std::string, std::string>>()); } -template <typename RHS_T> +template <typename T> void BinaryOperatorRegisterForString::_register_concat() { OperatorRepository& repository = OperatorRepository::instance(); - repository.addBinaryOperator<language::plus_op>(std::make_shared<ConcatExpressionProcessorBuilder<RHS_T>>()); + repository.addBinaryOperator<language::plus_op>(std::make_shared<ConcatExpressionProcessorBuilder<std::string, T>>()); + if constexpr (not std::is_same_v<T, std::string>) { + repository.addBinaryOperator<language::plus_op>( + std::make_shared<ConcatExpressionProcessorBuilder<T, std::string>>()); + } } BinaryOperatorRegisterForString::BinaryOperatorRegisterForString() diff --git a/src/language/utils/ConcatExpressionProcessorBuilder.hpp b/src/language/utils/ConcatExpressionProcessorBuilder.hpp index 8fed35962..e38fec5f4 100644 --- a/src/language/utils/ConcatExpressionProcessorBuilder.hpp +++ b/src/language/utils/ConcatExpressionProcessorBuilder.hpp @@ -8,16 +8,19 @@ #include <type_traits> -template <typename B_DataT> +template <typename A_DataT, typename B_DataT> class ConcatExpressionProcessorBuilder final : public IBinaryOperatorProcessorBuilder { + static_assert(std::is_same_v<A_DataT, std::string> or std::is_same_v<B_DataT, std::string>, + "one of the operand types must be an std::string"); + public: ConcatExpressionProcessorBuilder() = default; ASTNodeDataType getDataTypeOfA() const { - return ast_node_data_type_from<std::string>; + return ast_node_data_type_from<A_DataT>; } ASTNodeDataType @@ -35,7 +38,7 @@ class ConcatExpressionProcessorBuilder final : public IBinaryOperatorProcessorBu std::unique_ptr<INodeProcessor> getNodeProcessor(ASTNode& node) const { - return std::make_unique<ConcatExpressionProcessor<B_DataT>>(node); + return std::make_unique<ConcatExpressionProcessor<A_DataT, B_DataT>>(node); } }; diff --git a/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp b/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp index 175093d61..48a4af3d2 100644 --- a/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp +++ b/tests/test_ASTNodeBinaryOperatorExpressionBuilder.cpp @@ -463,15 +463,17 @@ x+y; CHECK_AST(data, result); } - SECTION("string concatenate bool") + SECTION("string concatenate B") { std::string_view data = R"( "foo"+true; )"; - std::string_view result = R"( + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( (root:ASTNodeListProcessor) - `-(language::plus_op:ConcatExpressionProcessor<bool>) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, bool>) +-(language::literal:"foo":ValueProcessor) `-(language::true_kw:ValueProcessor) )"; @@ -481,17 +483,18 @@ x+y; SECTION("string concatenate N") { - std::string_view data = R"( + std::string_view data = R"( let n : N, n=0; "foo"+n; )"; - - std::string_view result = R"( + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( (root:ASTNodeListProcessor) +-(language::eq_op:AffectationProcessor<language::eq_op, unsigned long, long>) | +-(language::name:n:NameProcessor) | `-(language::integer:0:ValueProcessor) - `-(language::plus_op:ConcatExpressionProcessor<unsigned long>) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, unsigned long>) +-(language::literal:"foo":ValueProcessor) `-(language::name:n:NameProcessor) )"; @@ -505,9 +508,11 @@ let n : N, n=0; "foo"+1; )"; - std::string_view result = R"( + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( (root:ASTNodeListProcessor) - `-(language::plus_op:ConcatExpressionProcessor<long>) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, long>) +-(language::literal:"foo":ValueProcessor) `-(language::integer:1:ValueProcessor) )"; @@ -521,9 +526,11 @@ let n : N, n=0; "foo"+1.2; )"; - std::string_view result = R"( + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( (root:ASTNodeListProcessor) - `-(language::plus_op:ConcatExpressionProcessor<double>) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, double>) +-(language::literal:"foo":ValueProcessor) `-(language::real:1.2:ValueProcessor) )"; @@ -531,6 +538,309 @@ let n : N, n=0; CHECK_AST(data, result); } + SECTION("string concatenate R^1") + { + std::string_view data = R"( +let x:R^1; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyVector<1ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^2") + { + std::string_view data = R"( +let x:R^2; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyVector<2ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^3") + { + std::string_view data = R"( +let x:R^3; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyVector<3ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^1x1") + { + std::string_view data = R"( +let x:R^1x1; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyMatrix<1ul, 1ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^2x2") + { + std::string_view data = R"( +let x:R^2x2; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyMatrix<2ul, 2ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^3x3") + { + std::string_view data = R"( +let x:R^3x3; +"foo"+x; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<)" + + string_name + R"(, TinyMatrix<3ul, 3ul, double> >) + +-(language::literal:"foo":ValueProcessor) + `-(language::name:x:NameProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("B concatenate string") + { + std::string_view data = R"( +false+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<bool, )" + + string_name + R"( >) + +-(language::false_kw:ValueProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("N concatenate string") + { + std::string_view data = R"( +let n : N, n=0; +n+"foo"; +)"; + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + +-(language::eq_op:AffectationProcessor<language::eq_op, unsigned long, long>) + | +-(language::name:n:NameProcessor) + | `-(language::integer:0:ValueProcessor) + `-(language::plus_op:ConcatExpressionProcessor<unsigned long, )" + + string_name + R"( >) + +-(language::name:n:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("Z concatenate string") + { + std::string_view data = R"( +1+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<long, )" + + string_name + R"( >) + +-(language::integer:1:ValueProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R concatenate string") + { + std::string_view data = R"( +1.2+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<double, )" + + string_name + R"( >) + +-(language::real:1.2:ValueProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R^1 concatenate string ") + { + std::string_view data = R"( +let x:R^1; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyVector<1ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R^2 concatenate string") + { + std::string_view data = R"( +let x:R^2; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyVector<2ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R^3 concatenate string") + { + std::string_view data = R"( +let x:R^3; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyVector<3ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R^1x1 concatenate string") + { + std::string_view data = R"( +let x:R^1x1; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyMatrix<1ul, 1ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("string concatenate R^2x2") + { + std::string_view data = R"( +let x:R^2x2; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyMatrix<2ul, 2ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + + SECTION("R^3x3 concatenate string") + { + std::string_view data = R"( +let x:R^3x3; +x+"foo"; +)"; + + std::string string_name = demangle(typeid(std::string{}).name()); + std::string result = R"( +(root:ASTNodeListProcessor) + `-(language::plus_op:ConcatExpressionProcessor<TinyMatrix<3ul, 3ul, double>, )" + + string_name + R"( >) + +-(language::name:x:NameProcessor) + `-(language::literal:"foo":ValueProcessor) +)"; + + CHECK_AST(data, result); + } + SECTION("string concatenate string") { std::string_view data = R"( @@ -542,7 +852,7 @@ let n : N, n=0; std::string result = R"( (root:ASTNodeListProcessor) `-(language::plus_op:ConcatExpressionProcessor<)" + - string_name + R"( >) + string_name + ", " + string_name + R"( >) +-(language::literal:"foo":ValueProcessor) `-(language::literal:"bar":ValueProcessor) )"; @@ -1649,16 +1959,17 @@ n >> m; "undefined binary operator type: string + void"); } - SECTION("right string plus") + SECTION("right string plus bad lhs") { auto ast = std::make_unique<ASTNode>(); ast->set_type<language::plus_op>(); ast->children.emplace_back(std::make_unique<ASTNode>()); ast->children.emplace_back(std::make_unique<ASTNode>()); - ast->children[0]->m_data_type = ASTNodeDataType::build<ASTNodeDataType::int_t>(); + ast->children[0]->m_data_type = ASTNodeDataType::build<ASTNodeDataType::void_t>(); ast->children[1]->m_data_type = ASTNodeDataType::build<ASTNodeDataType::string_t>(); - REQUIRE_THROWS_WITH(ASTNodeBinaryOperatorExpressionBuilder{*ast}, "undefined binary operator type: Z + string"); + REQUIRE_THROWS_WITH(ASTNodeBinaryOperatorExpressionBuilder{*ast}, + "undefined binary operator type: void + string"); } SECTION("lhs bad minus") diff --git a/tests/test_ASTNodeDataTypeBuilder.cpp b/tests/test_ASTNodeDataTypeBuilder.cpp index 1adbb89a1..6d66df083 100644 --- a/tests/test_ASTNodeDataTypeBuilder.cpp +++ b/tests/test_ASTNodeDataTypeBuilder.cpp @@ -2339,18 +2339,5 @@ true xor false; CHECK_AST(data, result); } - - SECTION("invalid operands") - { - std::string_view data = R"( -1+"string"; -)"; - - TAO_PEGTL_NAMESPACE::string_input input{data, "test.pgs"}; - auto ast = ASTBuilder::build(input); - ASTSymbolTableBuilder{*ast}; - REQUIRE_THROWS_WITH(ASTNodeDataTypeBuilder{*ast}, "undefined binary operator\n" - "note: incompatible operand types Z and string"); - } } } diff --git a/tests/test_ConcatExpressionProcessor.cpp b/tests/test_ConcatExpressionProcessor.cpp index 7e8d097f9..62d682249 100644 --- a/tests/test_ConcatExpressionProcessor.cpp +++ b/tests/test_ConcatExpressionProcessor.cpp @@ -61,23 +61,45 @@ TEST_CASE("ConcatExpressionProcessor", "[language]") CHECK_CONCAT_EXPRESSION_RESULT(R"(let n:N, n = 1; let s:string, s = "foo_"; s = s+n;)", "s", std::string{"foo_1"}); } + SECTION("N + string") + { + CHECK_CONCAT_EXPRESSION_RESULT(R"(let n:N, n = 1; let s:string, s = "_foo"; s = n+s;)", "s", std::string{"1_foo"}); + } + SECTION("string + Z") { CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "foo_"; s = s+2;)", "s", std::string{"foo_2"}); } + SECTION("Z + string") + { + CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "_foo"; s = 2+s;)", "s", std::string{"2_foo"}); + } + SECTION("string + R") { CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "foo_"; s = s+2.4;)", "s", std::string{"foo_"} + std::to_string(2.4)); } + SECTION("R + string") + { + CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "_foo"; s = 2.4+s;)", "s", + std::to_string(2.4) + std::string{"_foo"}); + } + SECTION("string + B") { CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "foo_"; s = s+(2>1);)", "s", std::string{"foo_true"}); CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "foo_"; s = s+(1>2);)", "s", std::string{"foo_false"}); } + SECTION("B + string") + { + CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "_foo"; s = (2>1)+s;)", "s", std::string{"true_foo"}); + CHECK_CONCAT_EXPRESSION_RESULT(R"(let s:string, s = "_foo"; s = (1>2)+s;)", "s", std::string{"false_foo"}); + } + SECTION("string + R^1") { std::ostringstream os; @@ -86,6 +108,14 @@ TEST_CASE("ConcatExpressionProcessor", "[language]") CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^1, x = 1; let s:string, s = "foo_"; s = s+x;)", "s", os.str()); } + SECTION("R^1 + string") + { + std::ostringstream os; + os << TinyVector<1>{1} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^1, x = 1; let s:string, s = "_foo"; s = x+s;)", "s", os.str()); + } + SECTION("string + R^2") { std::ostringstream os; @@ -94,6 +124,14 @@ TEST_CASE("ConcatExpressionProcessor", "[language]") CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^2, x = (1,2); let s:string, s = "foo_"; s = s+x;)", "s", os.str()); } + SECTION(" R^2 + string") + { + std::ostringstream os; + os << TinyVector<2>{1, 2} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^2, x = (1,2); let s:string, s = "_foo"; s = x+s;)", "s", os.str()); + } + SECTION("string + R^3") { std::ostringstream os; @@ -101,4 +139,62 @@ TEST_CASE("ConcatExpressionProcessor", "[language]") CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^3, x = (1,2,3); let s:string, s = "foo_"; s = s+x;)", "s", os.str()); } + + SECTION("R^3 + string") + { + std::ostringstream os; + os << TinyVector<3>{1, 2, 3} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^3, x = (1,2,3); let s:string, s = "_foo"; s = x+s;)", "s", os.str()); + } + + SECTION("string + R^1x1") + { + std::ostringstream os; + os << "foo_" << TinyMatrix<1>{1}; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^1x1, x = 1; let s:string, s = "foo_"; s = s+x;)", "s", os.str()); + } + + SECTION("R^1x1 + string") + { + std::ostringstream os; + os << TinyMatrix<1>{1} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^1x1, x = 1; let s:string, s = "_foo"; s = x+s;)", "s", os.str()); + } + + SECTION("string + R^2x2") + { + std::ostringstream os; + os << "foo_" << TinyMatrix<2>{1, 2, 3, 4}; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^2x2, x = (1,2,3,4); let s:string, s = "foo_"; s = s+x;)", "s", os.str()); + } + + SECTION(" R^2x2 + string") + { + std::ostringstream os; + os << TinyMatrix<2>{1, 2, 3, 4} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^2x2, x = (1,2,3,4); let s:string, s = "_foo"; s = x+s;)", "s", os.str()); + } + + SECTION("string + R^3x3") + { + std::ostringstream os; + os << "foo_" << TinyMatrix<3>{1, 2, 3, 4, 5, 6, 7, 8, 9}; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^3x3, x = (1,2,3,4,5,6,7,8,9); let s:string, s = "foo_"; s = s+x;)", "s", + os.str()); + } + + SECTION("R^3x3 + string") + { + std::ostringstream os; + os << TinyMatrix<3>{1, 2, 3, 4, 5, 6, 7, 8, 9} << "_foo"; + + CHECK_CONCAT_EXPRESSION_RESULT(R"(let x:R^3x3, x = (1,2,3,4,5,6,7,8,9); let s:string, s = "_foo"; s = x+s;)", "s", + os.str()); + } } -- GitLab