diff --git a/Makefile b/Makefile index 67d8c27..af5dcec 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ nanobind_tests: -Dnanobind_DIR=$$(python3 -m nanobind --cmake_dir) cmake --build "$(NB_TESTS_BUILD_DIR)" NB_SO_DIR="$(NB_TESTS_BUILD_DIR)" \ - pytest tests/nanobind/ + pytest tests/nanobind/pytest release_test: uv sync --no-default-groups --no-install-project diff --git a/nanobind/include/bind_vector.hpp b/nanobind/include/bind_vector.hpp index 7646521..22572e4 100644 --- a/nanobind/include/bind_vector.hpp +++ b/nanobind/include/bind_vector.hpp @@ -87,4 +87,4 @@ void bind_array(nb::module_& m, char const* name) { } // namespace coconext_nb -#endif // NB_BIND_ARRAY_HPP +#endif // NB_BIND_VECTOR_HPP diff --git a/nanobind/include/type_cast_array.hpp b/nanobind/include/type_cast_array.hpp new file mode 100644 index 0000000..c66d850 --- /dev/null +++ b/nanobind/include/type_cast_array.hpp @@ -0,0 +1,139 @@ +#ifndef NB_TYPE_CAST_ARRAY_HPP +#define NB_TYPE_CAST_ARRAY_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace nanobind::detail { + +using namespace coconext::types; +using coconext::types::detail::Array; + +template +struct type_caster> { + private: + using Value = Array; + using index_t = typename Value::index_type; + std::optional value; + + public: + static constexpr auto Name = + const_name("cocotb.types.Array[") + make_caster::Name + const_name("]"); + + template + using Cast = movable_cast_t; + + // Python -> C++ (Dynamic Python Array -> Static C++ Array) + bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) noexcept { + try { + if (!hasattr(src, "range")) { + return false; + } + + object py_range = src.attr("range"); + index_t left = cast(py_range.attr("left")); + index_t right = cast(py_range.attr("right")); + std::string dir_str = cast(py_range.attr("direction")); + auto direction = to_direction(dir_str); + + Range py_c_range{left, direction, right}; + + if (py_c_range.length() != R.length()) { + return false; + } + + if (!isinstance(src)) { + return false; + } + + constexpr size_t N = R.length(); + value.emplace(); + + make_caster item_caster; + auto it = value->begin(); + size_t count = 0; + + for (handle item : borrow(src)) { + if (count >= N) { + value.reset(); + return false; + } + + if (!item_caster.from_python( + item, flags & ~nanobind::detail::cast_flags::convert, cleanup + )) + { + value.reset(); + return false; + } + + *it = std::move(item_caster.operator Cast()); + ++it; + count++; + } + + if (count != N) { + value.reset(); + return false; + } + + return true; + } catch (std::exception const& e) { + fprintf(stderr, "C++ Exception in from_python: %s\n", e.what()); + return false; + } catch (...) { + return false; + } + } + + // C++ -> Python (Static C++ Array -> Dynamic Python Array) + static handle from_cpp( + Value const& src, rv_policy policy, cleanup_list* cleanup + ) noexcept { + try { + module_ cocotb_types = module_::import_("cocotb.types"); + object py_Array = cocotb_types.attr("Array"); + object py_Range = cocotb_types.attr("Range"); + + list py_list; + for (auto const& item : src) { + py_list.append(nanobind::cast(item, policy)); + } + + object cpp_range = nanobind::cast(src.range(), policy); + + index_t left = nanobind::cast(cpp_range.attr("left")); + index_t right = nanobind::cast(cpp_range.attr("right")); + std::string_view py_dir_str = to_string(src.range().direction); + + object pure_py_range = py_Range(left, std::string{py_dir_str}, right); + object result = py_Array(py_list, nanobind::arg("range") = pure_py_range); + return result.release(); + + } catch (python_error& e) { + e.restore(); + return handle(); + } catch (std::exception const& e) { + PyErr_SetString(PyExc_RuntimeError, e.what()); + return handle(); + } catch (...) { + PyErr_SetString(PyExc_RuntimeError, "Unknown C++ exception inside from_cpp"); + return handle(); + } + } + + explicit operator Value*() { return &(*value); } + explicit operator Value&() { return *value; } + explicit operator Value&&() { return std::move(*value); } +}; + +} // namespace nanobind::detail + +#endif // NB_TYPE_CAST_ARRAY_HPP diff --git a/nanobind/include/type_cast_vector.hpp b/nanobind/include/type_cast_vector.hpp new file mode 100644 index 0000000..72bb56d --- /dev/null +++ b/nanobind/include/type_cast_vector.hpp @@ -0,0 +1,137 @@ +#ifndef NB_TYPE_CAST_VECTOR_HPP +#define NB_TYPE_CAST_VECTOR_HPP + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace nanobind::detail { + +using namespace coconext::types; + +template +struct type_caster> { + private: + using Value = Vector; + using index_t = typename Value::index_type; + std::optional value; + + public: + static constexpr auto Name = + const_name("cocotb.types.Array[") + make_caster::Name + const_name("]"); + + template + using Cast = movable_cast_t; + + // Python -> C++ (Array to Vector) + bool from_python(handle src, uint8_t flags, cleanup_list* cleanup) noexcept { + try { + if (!hasattr(src, "range")) { + return false; + } + + object py_range = src.attr("range"); + index_t left = cast(py_range.attr("left")); + index_t right = cast(py_range.attr("right")); + std::string dir_str = cast(py_range.attr("direction")); + auto direction = to_direction(dir_str); + + Range c_range{left, direction, right}; + + if (!isinstance(src)) { + return false; + } + + value.emplace(c_range); + make_caster item_caster; + auto it = value->begin(); + size_t count = 0; + size_t expected_len = c_range.length(); + + for (handle item : borrow(src)) { + if (count >= expected_len) { + value.reset(); + return false; + } + + if (!item_caster.from_python( + item, flags & ~nanobind::detail::cast_flags::convert, cleanup + )) + { + value.reset(); + return false; + } + + *it = std::move(item_caster.operator Cast()); + ++it; + ++count; + } + + if (count != expected_len) { + value.reset(); + return false; + } + + return true; + } catch (std::exception const& e) { + fprintf(stderr, "C++ Exception caught: %s\n", e.what()); + return false; + } catch (...) { + return false; + } + } + + // C++ -> Python (Vector to Array) + static handle from_cpp( + Value const& src, rv_policy policy, cleanup_list* cleanup + ) noexcept { + try { + module_ cocotb_types = module_::import_("cocotb.types"); + object py_Array = cocotb_types.attr("Array"); + object py_Range = cocotb_types.attr("Range"); + + list py_list; + for (auto const& item : src) { + py_list.append(nanobind::cast(item, policy)); + } + + object cpp_range = nanobind::cast(src.range(), policy); + + index_t left = nanobind::cast(cpp_range.attr("left")); + index_t right = nanobind::cast(cpp_range.attr("right")); + std::string_view py_dir_str = to_string(src.range().direction); + + object pure_py_range = py_Range(left, std::string{py_dir_str}, right); + + object result = py_Array(py_list, nanobind::arg("range") = pure_py_range); + return result.release(); + + } catch (python_error& e) { + // If a Python exception occurred, restore it so pytest shows the exact + // traceback + e.restore(); + return handle(); + } catch (std::exception const& e) { + // Surface C++ exceptions (like nanobind::cast_error) directly to Python + PyErr_SetString(PyExc_RuntimeError, e.what()); + return handle(); + } catch (...) { + PyErr_SetString(PyExc_RuntimeError, "Unknown C++ exception inside from_cpp"); + return handle(); + } + } + + explicit operator Value*() { return &(*value); } + explicit operator Value&() { return *value; } + explicit operator Value&&() { return std::move(*value); } +}; + +} // namespace nanobind::detail + +#endif // NB_TYPE_CAST_VECTOR_HPP diff --git a/tests/nanobind/CMakeLists.txt b/tests/nanobind/CMakeLists.txt index f06306a..3fbee03 100644 --- a/tests/nanobind/CMakeLists.txt +++ b/tests/nanobind/CMakeLists.txt @@ -12,8 +12,10 @@ nanobind_add_module( nanobind_tests STABLE_ABI NB_STATIC - bind_vector/bind_typed_vector.cpp ../../nanobind/src/types/bind_range.cpp + bind.cpp + type_cast_vector.cpp + type_cast_array.cpp ) target_include_directories(nanobind_tests PRIVATE ../../cpp/include) diff --git a/tests/nanobind/bind.cpp b/tests/nanobind/bind.cpp new file mode 100644 index 0000000..f3abcff --- /dev/null +++ b/tests/nanobind/bind.cpp @@ -0,0 +1,22 @@ +#include "../../nanobind/include/bind_vector.hpp" +#include +#include +#include + +namespace nb = nanobind; +using namespace coconext::types; + +void register_range(nb::module_& m); +void init_test_vector_caster(nb::module_& m); +void init_test_array_caster(nb::module_& m); + +NB_MODULE(nanobind_tests, m) { + + register_range(m); + + coconext_nb::bind_array>(m, "IntVector"); + coconext_nb::bind_array>(m, "StringVector"); + + init_test_vector_caster(m); + init_test_array_caster(m); +} diff --git a/tests/nanobind/bind_vector/bind_typed_vector.cpp b/tests/nanobind/bind_vector/bind_typed_vector.cpp deleted file mode 100644 index 52f16e1..0000000 --- a/tests/nanobind/bind_vector/bind_typed_vector.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "../../../nanobind/include/bind_vector.hpp" -#include -#include -#include - -namespace nb = nanobind; - -void register_range(nb::module_& m); - -NB_MODULE(nanobind_tests, m) { - - register_range(m); - - coconext_nb::bind_array>(m, "IntVector"); - coconext_nb::bind_array>(m, "StringVector"); -} diff --git a/tests/nanobind/conftest.py b/tests/nanobind/pytest/conftest.py similarity index 100% rename from tests/nanobind/conftest.py rename to tests/nanobind/pytest/conftest.py diff --git a/tests/nanobind/pytest/test_array_caster.py b/tests/nanobind/pytest/test_array_caster.py new file mode 100644 index 0000000..8740f83 --- /dev/null +++ b/tests/nanobind/pytest/test_array_caster.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import nanobind_tests +import pytest +from cocotb.types import Array, Range + +ext = nanobind_tests.test_array_caster_ext + + +def test_array_element_wise_add(): + """Test successful translation Python -> C++ -> Python.""" + + r = Range(3, "downto", 0) + + a: Array[int] = Array([1, 2, 3, 4], range=r) + b: Array[int] = Array([100, 95, 89, 67], range=r) + + # call test cpp function (Python -> C++ -> Python) + c = ext.element_wise_add_array(a, b) + + assert isinstance(c, Array) + assert c == [101, 97, 92, 71] + assert c.range == r + + +def test_array_element_wise_add_without_range(): + """Test successful translation from Python -> C++ -> Python(Unspecified Range).""" + a: Array[int] = Array([1, 2, 3, 4]) + b: Array[int] = Array([100, 95, 89, 67]) + + # call test cpp function (Python -> C++ -> Python) + c = ext.element_wise_add_array(a, b) + + assert isinstance(c, Array) + assert c == [101, 97, 92, 71] + + +def test_array_bounds_mismatch_throws(): + """Passing an Array with the wrong static size fails nanobind type casting.""" + + r_wrong = Range(2, "downto", 0) + + a = Array([1, 2, 3], range=r_wrong) + b = Array([1, 2, 3], range=r_wrong) + + with pytest.raises(TypeError): + ext.element_wise_add_array(a, b) + + +def test_array_invalid_type_throws(): + """Only cocotb.types.Array is compatible with our test cpp function""" + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + + with pytest.raises(TypeError): + ext.element_wise_add_array(a, b) + + +def test_array_element_invalid_type_throws(): + """Incompatible array element type must raise a type error""" + a = Array([1, 2, 3, "4"]) + b = Array([1, 2, 3, 4]) + + with pytest.raises(TypeError): + ext.element_wise_add_array(a, b) diff --git a/tests/nanobind/bind_vector/test_typed_vector.py b/tests/nanobind/pytest/test_typed_vector.py similarity index 100% rename from tests/nanobind/bind_vector/test_typed_vector.py rename to tests/nanobind/pytest/test_typed_vector.py diff --git a/tests/nanobind/pytest/test_vector_caster.py b/tests/nanobind/pytest/test_vector_caster.py new file mode 100644 index 0000000..460ef10 --- /dev/null +++ b/tests/nanobind/pytest/test_vector_caster.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import nanobind_tests +import pytest +from cocotb.types import Array, Range + +ext = nanobind_tests.test_vector_caster_ext + + +def test_vector_element_wise_add(): + """Test successful translation from Python -> C++ -> Python.""" + + r = Range(3, "downto", 0) + + a: Array[int] = Array([1, 2, 3, 4], range=r) + b: Array[int] = Array([100, 95, 89, 67], range=r) + + c = ext.element_wise_add(a, b) + + assert isinstance(c, Array), ( + "Return type should automatically cast to cocotb.types.Array" + ) + + assert c == [101, 97, 92, 71] + assert c.range == r + + +def test_vector_element_wise_add_without_range(): + """Test successful translation from Python -> C++ -> Python(Unspecified Range).""" + + c: Array[int] = Array([1, 2, 3, 4]) + d: Array[int] = Array([100, 95, 89, 67]) + + c = ext.element_wise_add(c, d) + + assert isinstance(c, Array), ( + "Return type should automatically cast to cocotb.types.Array" + ) + + assert c == [101, 97, 92, 71] + + +def test_vector_mismatched_ranges_throws(): + """Verify standard C++ exceptions map correctly to Python exceptions.""" + + a = Array([1, 2, 3]) # Length 3 + b = Array([1, 2, 3, 4]) # Length 4 + + with pytest.raises(ValueError): + ext.element_wise_add(a, b) + + +def test_array_invalid_type_throws(): + """Only cocotb.types.Array is compatible with our test cpp function""" + a = [1, 2, 3, 4] + b = [1, 2, 3, 4] + + with pytest.raises(TypeError): + ext.element_wise_add(a, b) + + +def test_array_element_invalid_type_throws(): + """Incompatible array element type must raise a type error""" + a = Array([1, 2, 3, "4"]) + b = Array([1, 2, 3, 4]) + + with pytest.raises(TypeError): + ext.element_wise_add(a, b) diff --git a/tests/nanobind/type_cast_array.cpp b/tests/nanobind/type_cast_array.cpp new file mode 100644 index 0000000..7dbf0d6 --- /dev/null +++ b/tests/nanobind/type_cast_array.cpp @@ -0,0 +1,34 @@ +#include "../../nanobind/include/type_cast_array.hpp" +#include +#include + +namespace nb = nanobind; +using namespace coconext::types; + +using TestArray4 = Array; + +TestArray4 element_wise_add_array(TestArray4 const& a, TestArray4 const& b) { + TestArray4 res; + + auto a_it = a.begin(); + auto b_it = b.begin(); + auto res_it = res.begin(); + + for (; a_it != a.end(); ++a_it, ++b_it, ++res_it) { + *res_it = *a_it + *b_it; + } + + return res; +} + +void init_test_array_caster(nb::module_& m) { + nb::module_ sm = m.def_submodule("test_array_caster_ext", "Array caster tests"); + + sm.def( + "element_wise_add_array", + &element_wise_add_array, + "Add two fixed-size Arrays element-wise", + nb::arg("a"), + nb::arg("b") + ); +} diff --git a/tests/nanobind/type_cast_vector.cpp b/tests/nanobind/type_cast_vector.cpp new file mode 100644 index 0000000..48655d1 --- /dev/null +++ b/tests/nanobind/type_cast_vector.cpp @@ -0,0 +1,35 @@ +#include "../../nanobind/include/type_cast_vector.hpp" +#include +#include + +namespace nb = nanobind; +using namespace coconext::types; + +Vector element_wise_add(Vector const& a, Vector const& b) { + if (a.range() != b.range()) { + throw std::invalid_argument("Lengths/Ranges must match"); + } + + Vector res(a.range()); + auto a_it = a.begin(); + auto b_it = b.begin(); + auto res_it = res.begin(); + + for (; a_it != a.end(); ++a_it, ++b_it, ++res_it) { + *res_it = *a_it + *b_it; + } + + return res; +} + +void init_test_vector_caster(nb::module_& m) { + nb::module_ sm = m.def_submodule("test_vector_caster_ext", "Vector caster tests"); + + sm.def( + "element_wise_add", + &element_wise_add, + "Add two Vectors element-wise", + nb::arg("a"), + nb::arg("b") + ); +}