diff --git a/blocks/math/CMakeLists.txt b/blocks/math/CMakeLists.txt
index 643e822d..d8a9d909 100644
--- a/blocks/math/CMakeLists.txt
+++ b/blocks/math/CMakeLists.txt
@@ -1,5 +1,5 @@
add_library(gr-math INTERFACE)
-target_link_libraries(gr-math INTERFACE gnuradio-core gnuradio-algorithm)
+target_link_libraries(gr-math INTERFACE gnuradio-core gnuradio-algorithm exprtk)
target_include_directories(gr-math INTERFACE $ $)
if (ENABLE_TESTING)
diff --git a/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp b/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp
new file mode 100644
index 00000000..955a9741
--- /dev/null
+++ b/blocks/math/include/gnuradio-4.0/math/ExpressionBlocks.hpp
@@ -0,0 +1,349 @@
+#ifndef EXPRESSIONBLOCKS_HPP
+#define EXPRESSIONBLOCKS_HPP
+
+#include
+#include
+#include
+
+#include
+
+namespace gr::blocks::math {
+
+namespace detail {
+
+inline std::string formatParserError(const auto& parser, std::string_view expression) {
+ std::stringstream ss;
+ for (std::size_t i = 0; i < parser.error_count(); ++i) {
+ const auto error = parser.get_error(i);
+
+ ss << fmt::format("ExprTk Parser Error({:2}): Position: {:2}\nType: [{:14}] Msg: {}; expression:\n{}\n", //
+ static_cast(i), //
+ static_cast(error.token.position), //
+ exprtk::parser_error::to_str(error.mode), error.diagnostic, expression);
+ }
+ return ss.str();
+}
+
+struct VectorInfo {
+ std::size_t size;
+ ssize_t index;
+};
+
+inline VectorInfo computeVectorInfo(void* base_ptr, void* end_ptr, std::size_t elementSize, void* access_ptr) {
+ if (!base_ptr || !end_ptr || !access_ptr) {
+ throw std::invalid_argument("null pointer(s) provided.");
+ }
+
+ auto base = static_cast(base_ptr);
+ auto end = static_cast(end_ptr);
+ auto access = static_cast(access_ptr);
+
+ if (end < base) {
+ throw std::out_of_range(fmt::format("invalid vector boundaries [{}, {}]", base_ptr, end_ptr));
+ }
+
+ return {static_cast(end - base) / elementSize, (access - base) / static_cast(elementSize)};
+}
+
+struct vector_access_rtc : public exprtk::vector_access_runtime_check {
+ std::unordered_map vector_map;
+
+ bool handle_runtime_violation(violation_context& context) override {
+ auto itr = vector_map.find(static_cast(context.base_ptr));
+ const std::string& vector_name = (itr != vector_map.end()) ? itr->second : "Unknown";
+
+ const auto typeSize = static_cast(context.type_size);
+ auto [vecSize, vecIndex] = computeVectorInfo(context.base_ptr, context.end_ptr, typeSize, context.access_ptr);
+ throw gr::exception(fmt::format("vector access '{name}[{index}]' outside of [0, {size}[ (typesize: {typesize})", //
+ fmt::arg("name", vector_name), fmt::arg("size", vecSize), fmt::arg("index", vecIndex), fmt::arg("typesize", typeSize)));
+ return false; // should never reach here
+ }
+};
+
+} // namespace detail
+
+template
+requires std::floating_point
+struct ExpressionSISO : Block> {
+ using Description = Doc;
+ template
+ using A = Annotated;
+
+ PortIn in;
+ PortOut out;
+
+ A> expr_string = "clamp(-1.0, sin(2 * pi * x) + cos(x / 2 * pi), +1.0)";
+ A, Visible> param_a = T(1.0);
+ A, Visible> param_b = T(0.0);
+ A, Visible> param_c = T(0.0);
+
+ GR_MAKE_REFLECTABLE(ExpressionSISO, in, out, expr_string, param_a, param_b, param_c);
+
+ exprtk::symbol_table _symbol_table;
+ exprtk::expression _expression;
+ T _in;
+ T _out;
+
+ void initExpression(std::source_location location = std::source_location::current()) {
+ reset();
+ _symbol_table.clear();
+ _symbol_table.add_variable("x", _in);
+ _symbol_table.add_variable("y", _out);
+
+ _symbol_table.add_variable(std::string(param_a.description()), param_a.value);
+ _symbol_table.add_variable(std::string(param_b.description()), param_b.value);
+ _symbol_table.add_variable(std::string(param_c.description()), param_c.value);
+
+ _symbol_table.add_constants();
+ _expression.register_symbol_table(_symbol_table);
+
+ if (exprtk::parser parser; !parser.compile(expr_string, _expression)) {
+ throw gr::exception(detail::formatParserError(parser, expr_string), location);
+ }
+ }
+
+ void settingsChanged(const property_map& /*oldSettings*/, const property_map& newSettings) {
+ if (newSettings.contains("expr_string")) {
+ initExpression();
+ }
+ }
+
+ void start() { initExpression(); }
+
+ void reset() {
+ _in = T(0);
+ _out = T(0);
+ }
+
+ [[nodiscard]] constexpr T processOne(T input) {
+ _in = input;
+ _out = _expression.value(); // evaluate expression, _out == 'y' defined to allow for recursion
+ return _out;
+ }
+};
+
+template
+requires std::floating_point
+struct ExpressionDISO : Block> {
+ using Description = Doc;
+ template
+ using A = Annotated;
+
+ PortIn in0;
+ PortIn in1;
+ PortOut out;
+
+ A> expr_string = "a*(x+y)";
+ A, Visible> param_a = T(1.0);
+ A, Visible> param_b = T(0.0);
+ A, Visible> param_c = T(0.0);
+
+ GR_MAKE_REFLECTABLE(ExpressionDISO, in0, in1, out, expr_string, param_a, param_b, param_c);
+
+ exprtk::symbol_table _symbol_table;
+ exprtk::expression _expression;
+ T _in0;
+ T _in1;
+ T _out;
+
+ void initExpression(std::source_location location = std::source_location::current()) {
+ reset();
+ _symbol_table.clear();
+ _symbol_table.add_variable("x", _in0);
+ _symbol_table.add_variable("y", _in1);
+ _symbol_table.add_variable("z", _out);
+
+ _symbol_table.add_variable(std::string(param_a.description()), param_a.value);
+ _symbol_table.add_variable(std::string(param_b.description()), param_b.value);
+ _symbol_table.add_variable(std::string(param_c.description()), param_c.value);
+
+ _symbol_table.add_constants();
+ _expression.register_symbol_table(_symbol_table);
+
+ if (exprtk::parser parser; !parser.compile(expr_string, _expression)) {
+ throw gr::exception(detail::formatParserError(parser, expr_string), location);
+ }
+ }
+
+ void settingsChanged(const property_map& /*oldSettings*/, const property_map& newSettings) {
+ if (newSettings.contains("expr_string")) {
+ initExpression();
+ }
+ }
+
+ void start() { initExpression(); };
+
+ void reset() {
+ _in0 = T(0);
+ _in1 = T(0);
+ _out = T(0);
+ }
+
+ [[nodiscard]] constexpr T processOne(T input0, T input1) {
+ _in0 = input0;
+ _in1 = input1;
+ _out = _expression.value(); // evaluate expression, _out == 'y' defined to allow for recursion
+ return _out;
+ }
+};
+
+template
+requires std::floating_point
+struct ExpressionBulk : public gr::Block> {
+ using Description = Doc;
+ template
+ using A = Annotated;
+
+ PortIn in;
+ PortOut out;
+
+ A> expr_string = "vecOut := a * vecIn;";
+ A, Visible> param_a = T(1.0);
+ A, Visible> param_b = T(0.0);
+ A, Visible> param_c = T(0.0);
+ A, Visible> runtime_checks = true;
+
+ GR_MAKE_REFLECTABLE(ExpressionBulk, in, out, expr_string, param_a, param_b, param_c, runtime_checks);
+
+ exprtk::symbol_table _symbol_table;
+ exprtk::expression _expression;
+ detail::vector_access_rtc _vec_rtc;
+
+ std::vector _vecInData;
+ std::vector _vecOutData;
+ // vector_views that reference _vecInData and _vecOutData
+ // will be registered once and then just rebased as needed.
+ // N.B. _maxBaseSize limits the maximum chunk size and needs
+ // to be defined in-advance due to ExprTk constraints
+ std::array _arrOutDummy{T(0)};
+ static constexpr std::size_t _maxBaseSize = 1UZ << 16;
+ exprtk::vector_view _vecIn = exprtk::make_vector_view(_arrOutDummy.data(), _maxBaseSize);
+ exprtk::vector_view _vecOut = exprtk::make_vector_view(_arrOutDummy.data(), _maxBaseSize);
+
+ void initExpression(std::source_location location = std::source_location::current()) {
+ _expression = exprtk::expression();
+ _symbol_table.clear();
+
+ if (_vecInData.empty()) {
+ _vecInData.resize(2UZ);
+ }
+ if (_vecOutData.empty()) {
+ _vecOutData.resize(2UZ);
+ }
+
+ // rebase vector views to current data
+ _vecIn.rebase(_vecInData.data());
+ _vecIn.set_size(_vecInData.size());
+ _vecOut.rebase(_vecOutData.data());
+ _vecOut.set_size(_vecOutData.size());
+
+ _symbol_table.add_vector("vecIn", _vecIn);
+ _symbol_table.add_vector("vecOut", _vecOut);
+
+ _symbol_table.add_variable(std::string(param_a.description()), param_a.value);
+ _symbol_table.add_variable(std::string(param_b.description()), param_b.value);
+ _symbol_table.add_variable(std::string(param_c.description()), param_c.value);
+
+ _symbol_table.add_constants();
+ _expression.register_symbol_table(_symbol_table);
+
+ exprtk::parser parser;
+ if (runtime_checks) {
+ _vec_rtc.vector_map[_vecIn.data()] = "vecIn";
+ _vec_rtc.vector_map[_vecOut.data()] = "vecOut";
+ parser.register_vector_access_runtime_check(_vec_rtc);
+ }
+
+ if (!parser.compile(expr_string, _expression)) {
+ throw gr::exception(detail::formatParserError(parser, expr_string), location);
+ }
+ }
+
+ void settingsChanged(const gr::property_map& /*oldSettings*/, const gr::property_map& newSettings) {
+ if (newSettings.contains("expr_string")) {
+ initExpression();
+ }
+ }
+
+ void start() { initExpression(); }
+
+ work::Status processBulk(InputSpanLike auto& inputSpan, OutputSpanLike auto& outputSpan) {
+ if (inputSpan.size() != _vecInData.size() || outputSpan.size() != _vecOutData.size()) {
+ _vecInData.resize(std::min(inputSpan.size(), _maxBaseSize));
+ _vecOutData.resize(std::min(outputSpan.size(), _maxBaseSize));
+
+ // rebase vector views to new internal memory storage of backing buffer vectors
+ _vecIn.rebase(_vecInData.data());
+ _vecIn.set_size(_vecInData.size());
+ _vecOut.rebase(_vecOutData.data());
+ _vecOut.set_size(_vecOutData.size());
+
+ if (runtime_checks) {
+ _vec_rtc.vector_map.clear();
+ _vec_rtc.vector_map[_vecIn.data()] = "vecIn";
+ _vec_rtc.vector_map[_vecOut.data()] = "vecOut";
+ }
+ }
+
+ using PtrDiff_t = std::iter_difference_t;
+ std::ranges::copy_n(inputSpan.begin(), static_cast(_vecInData.size()), _vecInData.begin());
+ _expression.value(); // evaluate expression, exception handled by caller
+ std::ranges::copy_n(_vecOutData.begin(), static_cast(_vecOutData.size()), outputSpan.begin());
+
+ std::ignore = inputSpan.consume(_vecInData.size());
+ outputSpan.publish(_vecOutData.size());
+ return work::Status::OK;
+ }
+};
+
+} // namespace gr::blocks::math
+
+// const inline static auto registerConstMath = gr::registerBlock(gr::globalBlockRegistry()) //
+// + gr::registerBlock(gr::globalBlockRegistry()) //
+// + gr::registerBlock(gr::globalBlockRegistry());
+
+#endif // EXPRESSIONBLOCKS_HPP
diff --git a/blocks/math/test/CMakeLists.txt b/blocks/math/test/CMakeLists.txt
index 29187d87..fdb63354 100644
--- a/blocks/math/test/CMakeLists.txt
+++ b/blocks/math/test/CMakeLists.txt
@@ -1 +1,2 @@
add_ut_test(qa_Math)
+add_ut_test(qa_ExpressionBlocks)
diff --git a/blocks/math/test/qa_ExpressionBlocks.cpp b/blocks/math/test/qa_ExpressionBlocks.cpp
new file mode 100644
index 00000000..4a6c6611
--- /dev/null
+++ b/blocks/math/test/qa_ExpressionBlocks.cpp
@@ -0,0 +1,105 @@
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+const boost::ut::suite<"basic expression block tests"> basicMath = [] {
+ using namespace boost::ut;
+ using namespace gr;
+ using namespace gr::blocks::math;
+ using testing::ProcessFunction::USE_PROCESS_ONE;
+
+ "ExpressionSISO"_test = [](const T&) {
+ Graph graph;
+
+ auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(21)}});
+ auto& exprBlock = graph.emplaceBlock>({{"expr_string", "a*x"}, {"param_a", T(2)}});
+ auto& tagSink = graph.emplaceBlock>({{"log_tags", true}, {"log_samples", true}});
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock)));
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink)));
+
+ auto sched = gr::scheduler::Simple<>(std::move(graph));
+ expect(sched.runAndWait().has_value());
+
+ expect(approx(source.default_value, T(21), T(1e-3f)));
+ expect(approx(exprBlock.param_a.value, T(2), T(1e-3f)));
+ expect(eq(tagSink._samples.size(), source.n_samples_max));
+ expect(approx(tagSink._samples[0], T(42), T(1e-3f)));
+ } | std::tuple{};
+
+ "ExpressionDISO"_test = [](const T&) {
+ Graph graph;
+
+ auto& source1 = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(7)}});
+ auto& source2 = graph.emplaceBlock>({{"n_samples_max", 10U}, {"default_value", T(5)}});
+ auto& exprBlock = graph.emplaceBlock>({{"expr_string", "z := a * (x + y + 2)"}, {"param_a", T(3)}});
+ auto& tagSink = graph.emplaceBlock>({{"log_tags", true}, {"log_samples", true}});
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source1).template to<"in0">(exprBlock)));
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source2).template to<"in1">(exprBlock)));
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink)));
+
+ auto sched = gr::scheduler::Simple<>(std::move(graph));
+ expect(sched.runAndWait().has_value());
+
+ expect(approx(source1.default_value, T(7), T(1e-3f)));
+ expect(approx(source2.default_value, T(5), T(1e-3f)));
+ expect(approx(exprBlock.param_a.value, T(3), T(1e-3f)));
+ expect(eq(tagSink._samples.size(), source1.n_samples_max));
+ expect(eq(tagSink._samples.size(), source2.n_samples_max));
+ expect(approx(tagSink._samples[0], T(42), T(1e-3f)));
+ } | std::tuple{};
+
+ "ExpressionBulk"_test = [](const T&) {
+ Graph graph;
+
+ auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}});
+ auto& exprBlock = graph.emplaceBlock>({{"expr_string", "vecOut := a * vecIn"}, {"param_a", T(2)}});
+ auto& tagSink = graph.emplaceBlock>({{"log_samples", true}});
+
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock)));
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink)));
+
+ auto sched = gr::scheduler::Simple<>(std::move(graph));
+ expect(sched.runAndWait().has_value());
+
+ expect(eq(tagSink._samples.size(), source.n_samples_max));
+ expect(approx(exprBlock.param_a.value, T(2), T(1e-3f)));
+ for (std::size_t i = 0; i < tagSink._samples.size(); ++i) {
+ T expected = T(1 + i) * T(2);
+ expect(approx(tagSink._samples[i], expected, T(1e-6))) << fmt::format("should be: output[{}] ({}) == {} * input[{}] ({}) ", i, tagSink._samples[i], exprBlock.param_a, i, expected);
+ }
+ } | std::tuple{};
+
+ "ExpressionBulk - exceptions"_test = [](const bool enableERuntimeChecks) {
+ Graph graph;
+
+ auto& source = graph.emplaceBlock>({{"n_samples_max", 10U}});
+ const std::string exprStr = R""(for (var i := 0; i < 100; i += 1) {
+ vecOut[i] := vecIn[i+1];
+}
+)"";
+ auto& exprBlock = graph.emplaceBlock>({{"expr_string", exprStr}, {"runtime_checks", enableERuntimeChecks}});
+ auto& tagSink = graph.emplaceBlock>({{"log_samples", true}});
+
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(source).template to<"in">(exprBlock)));
+ expect(eq(gr::ConnectionResult::SUCCESS, graph.connect<"out">(exprBlock).template to<"in">(tagSink)));
+
+ auto sched = gr::scheduler::Simple<>(std::move(graph));
+
+ try {
+ expect(sched.runAndWait().has_value());
+ expect(false) << fmt::format("should have failed");
+ } catch (const gr::exception& ex) {
+ expect(true);
+ fmt::println("failed correctly with:\n{}\n", ex);
+ } catch (...) {
+ expect(false) << fmt::format("caught unknown/unexpected exception");
+ }
+ } | std::vector{true/*, false -- disabled on purpose as this would trigger correctly trigger the ASAN checks*/};
+};
+
+int main() { /* not needed for UT */ }
diff --git a/blocks/math/test/qa_Math.cpp b/blocks/math/test/qa_Math.cpp
index 68ac0fcd..f0158fa1 100644
--- a/blocks/math/test/qa_Math.cpp
+++ b/blocks/math/test/qa_Math.cpp
@@ -144,4 +144,4 @@ std::complex, std::complex*/>();
} | kArithmeticTypes;
};
-int main() { /* not needed for UT */ }
\ No newline at end of file
+int main() { /* not needed for UT */ }
diff --git a/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp b/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp
index 1feef960..8d5cfb4f 100644
--- a/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp
+++ b/blocks/testing/include/gnuradio-4.0/testing/NullSources.hpp
@@ -35,7 +35,7 @@ Commonly used for testing and simulations where consistent output and finite exe
Annotatedn_samples_max -> signal DONE (0: infinite)">> n_samples_max = 0U;
Annotated> count = 0U;
- GR_MAKE_REFLECTABLE(ConstantSource, out, n_samples_max, count);
+ GR_MAKE_REFLECTABLE(ConstantSource, out, default_value, n_samples_max, count);
void reset() { count = 0U; }
@@ -91,7 +91,7 @@ Commonly used for testing and simulations where consistent output and finite exe
Annotatedn_samples_max -> signal DONE (0: infinite)">> n_samples_max = 0U;
Annotated> count = 0U;
- GR_MAKE_REFLECTABLE(CountingSource, out, n_samples_max, count);
+ GR_MAKE_REFLECTABLE(CountingSource, out, default_value, n_samples_max, count);
void reset() { count = 0U; }