From f5e56c2eedd45fa598900bb94481a9851e815f1b Mon Sep 17 00:00:00 2001 From: Archie S Date: Fri, 10 Apr 2026 17:40:22 +0100 Subject: [PATCH 1/7] Setup build system --- BUILD.md | 2 ++ CMakeLists.txt | 16 +++++++++++ dependencies/CMakeLists.txt | 21 ++++++++++---- dependencies/nanobind.cmake | 37 +++++++++++++++++++++++++ lib/bindings/python/CMakeLists.txt | 21 ++++++++++++++ lib/bindings/python/PythonInterface.cpp | 7 +++++ lib/bindings/python/PythonInterface.hpp | 0 7 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 dependencies/nanobind.cmake create mode 100644 lib/bindings/python/CMakeLists.txt create mode 100644 lib/bindings/python/PythonInterface.cpp create mode 100644 lib/bindings/python/PythonInterface.hpp diff --git a/BUILD.md b/BUILD.md index 801edb0..d6ddb8b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -32,6 +32,7 @@ Pass these to the `cmake -S dependencies` step: | `USE_EXTERNAL_LLVM` | ON | Use system LLVM (OFF = build from source) | | `USE_EXTERNAL_ABSEIL` | OFF | Use system abseil (OFF = build from source) | | `USE_EXTERNAL_HIGHWAY` | OFF | Use system highway (OFF = build from source) | +| `USE_EXTERNAL_NANOBIND` | OFF | Use system nanobind (OFF = build from source) | | `COBRA_BUILD_TESTS` | OFF | Build GoogleTest for tests | | `USE_EXTERNAL_GOOGLETEST` | OFF | Use system GoogleTest (OFF = build from source) | | `COBRA_ENABLE_Z3` | OFF | Enable Z3 support (requires lib/verify implementation) | @@ -45,6 +46,7 @@ Pass these to the `cmake -S .` step: |--------|---------|-------------| | `COBRA_BUILD_LLVM_PASS` | OFF | Build the LLVM pass plugin (requires LLVM 19-22) | | `COBRA_BUILD_TESTS` | OFF | Build tests (requires GoogleTest in prefix) | +| `COBRA_BUILD_PYTHON_BINDINGS` | OFF | Builds the Python module (requires nanobind) | ## With LLVM Pass Plugin diff --git a/CMakeLists.txt b/CMakeLists.txt index 4058690..da2445a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,17 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +if (CMAKE_EXPORT_COMPILE_COMMANDS AND UNIX) + set(_cobra_cc_src "${CMAKE_SOURCE_DIR}/compile_commands.json") + set(_cobra_cc_bin "${CMAKE_BINARY_DIR}/compile_commands.json") + if (NOT EXISTS "${_cobra_cc_src}") + execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink "${_cobra_cc_bin}" "${_cobra_cc_src}" + OUTPUT_QUIET ERROR_QUIET + ) + endif() +endif() + include(GNUInstallDirs) include(CMakePackageConfigHelpers) @@ -57,6 +68,7 @@ add_link_options( option(COBRA_BUILD_LLVM_PASS "Build the LLVM pass plugin (requires LLVM 19-22)" OFF) option(COBRA_BUILD_TESTS "Build tests (requires GoogleTest in prefix)" OFF) +option(COBRA_BUILD_PYTHON_BINDINGS "Build Python bindings (nanobind)" OFF) option(COBRA_ENABLE_TRACE "Enable detailed pipeline tracing to stderr (debug builds)" OFF) option(COBRA_ENABLE_TRACY "Enable Tracy profiler instrumentation" OFF) @@ -93,6 +105,10 @@ if(COBRA_BUILD_LLVM_PASS) add_subdirectory(lib/llvm) endif() +if(COBRA_BUILD_PYTHON_BINDINGS) + add_subdirectory(lib/bindings/python) +endif() + if(COBRA_BUILD_TESTS) enable_testing() add_subdirectory(test) diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 1754880..1e998b0 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -6,12 +6,15 @@ # cmake --build build-deps # # Options: -# USE_EXTERNAL_LLVM (default ON) - Use system LLVM vs build from source -# USE_EXTERNAL_HIGHWAY (default OFF) - Use system highway vs build from source -# COBRA_BUILD_TESTS (default OFF) - Build GoogleTest for tests -# USE_EXTERNAL_GOOGLETEST (default OFF) - Use system GoogleTest vs build from source -# COBRA_ENABLE_Z3 (default OFF) - Enable Z3 dependency -# USE_EXTERNAL_Z3 (default OFF) - Use system Z3 vs build from source +# USE_EXTERNAL_LLVM (default ON) - Use system LLVM vs build from source +# USE_EXTERNAL_ABSEIL (default OFF) - Use system abseil vs build from source +# USE_EXTERNAL_HIGHWAY (default OFF) - Use system highway vs build from source +# COBRA_BUILD_PYTHON_BINDINGS (default OFF) - Build nanobind for Python bindings +# USE_EXTERNAL_NANOBIND (default OFF) - Use system nanobind vs build from source +# COBRA_BUILD_TESTS (default OFF) - Build GoogleTest for tests +# USE_EXTERNAL_GOOGLETEST (default OFF) - Use system GoogleTest vs build from source +# COBRA_ENABLE_Z3 (default OFF) - Enable Z3 dependency +# USE_EXTERNAL_Z3 (default OFF) - Use system Z3 vs build from source cmake_minimum_required(VERSION 3.20) project(cobra-dependencies LANGUAGES C CXX) @@ -19,6 +22,8 @@ project(cobra-dependencies LANGUAGES C CXX) option(USE_EXTERNAL_LLVM "Use system LLVM instead of building from source" ON) option(USE_EXTERNAL_ABSEIL "Use system abseil instead of building from source" OFF) option(USE_EXTERNAL_HIGHWAY "Use system highway instead of building from source" OFF) +option(COBRA_BUILD_PYTHON_BINDINGS "Build nanobind for Python bindings" OFF) +option(USE_EXTERNAL_NANOBIND "Use system nanobind instead of building from source" OFF) option(COBRA_BUILD_TESTS "Build GoogleTest for CoBRA tests" OFF) option(USE_EXTERNAL_GOOGLETEST "Use system GoogleTest instead of building from source" OFF) option(COBRA_ENABLE_Z3 "Enable Z3 dependency (requires lib/verify to be implemented)" OFF) @@ -30,6 +35,10 @@ include(abseil.cmake) include(highway.cmake) include(llvm.cmake) +if(COBRA_BUILD_PYTHON_BINDINGS) + include(nanobind.cmake) +endif() + if(COBRA_BUILD_TESTS) include(googletest.cmake) endif() diff --git a/dependencies/nanobind.cmake b/dependencies/nanobind.cmake new file mode 100644 index 0000000..a8c424c --- /dev/null +++ b/dependencies/nanobind.cmake @@ -0,0 +1,37 @@ +# dependencies/nanobind.cmake +# nanobind dependency: forwarding config (external) or source build. + +set(_nanobind_config_dir "${COBRA_INSTALL_PREFIX}/lib/cmake/nanobind") +file(MAKE_DIRECTORY "${_nanobind_config_dir}") + +if(USE_EXTERNAL_NANOBIND) + find_package(nanobind CONFIG REQUIRED) + message(STATUS "Using external nanobind") + + file(WRITE "${_nanobind_config_dir}/nanobind-config.cmake" + "# Forwarding config - delegates to system nanobind\n" + "include(\"${nanobind_DIR}/nanobind-config.cmake\")\n" + ) + + cobra_mark_satisfied(nanobind) +else() + message(STATUS "Building nanobind from source (v2.12.0)") + cobra_add_dependency(nanobind + GIT_REPOSITORY https://github.com/wjakob/nanobind.git + GIT_TAG 2a61ad2494d09fecb2e13322c1383342c299900d # v2.12.0 + GIT_SHALLOW ON + GIT_PROGRESS ON + GIT_SUBMODULES_RECURSE ON + CMAKE_ARGS + ${COBRA_COMMON_CMAKE_ARGS} + -DNB_TEST=OFF + -DNB_CREATE_INSTALL_RULES=ON + -DNB_USE_SUBMODULE_DEPS=ON + -DCMAKE_POSITION_INDEPENDENT_CODE=ON + ) + + file(WRITE "${_nanobind_config_dir}/nanobind-config.cmake" + "# Forwarding config - delegates to installed nanobind\n" + "include(\"${COBRA_INSTALL_PREFIX}/nanobind/cmake/nanobind-config.cmake\")\n" + ) +endif() diff --git a/lib/bindings/python/CMakeLists.txt b/lib/bindings/python/CMakeLists.txt new file mode 100644 index 0000000..bfe6d75 --- /dev/null +++ b/lib/bindings/python/CMakeLists.txt @@ -0,0 +1,21 @@ +find_package(Python 3.10 COMPONENTS Interpreter Development REQUIRED) + +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() + +if (NOT nanobind_DIR AND NOT nanobind_ROOT) + execute_process( + COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir + OUTPUT_STRIP_TRAILING_WHITESPACE OUTPUT_VARIABLE nanobind_ROOT + ) +endif() +if (nanobind_ROOT AND NOT nanobind_DIR) + set(nanobind_DIR "${nanobind_ROOT}") +endif() +find_package(nanobind CONFIG REQUIRED) + +nanobind_add_module(cobra_mba PythonInterface.cpp PythonInterface.hpp) + + diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp new file mode 100644 index 0000000..bd25804 --- /dev/null +++ b/lib/bindings/python/PythonInterface.cpp @@ -0,0 +1,7 @@ +#include + +int add(int a, int b) {return a + b;} + +NB_MODULE(cobra_mba, m) { + m.def("add", &add); +} diff --git a/lib/bindings/python/PythonInterface.hpp b/lib/bindings/python/PythonInterface.hpp new file mode 100644 index 0000000..e69de29 From 0a512409f83103b06f9023ae16d23602347f35aa Mon Sep 17 00:00:00 2001 From: Archie S Date: Fri, 10 Apr 2026 22:22:25 +0100 Subject: [PATCH 2/7] Created basic python types --- lib/bindings/python/CMakeLists.txt | 9 +++- lib/bindings/python/CobraPython.cpp | 70 +++++++++++++++++++++++++ lib/bindings/python/CobraPython.hpp | 29 ++++++++++ lib/bindings/python/PythonInterface.cpp | 31 ++++++++++- 4 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 lib/bindings/python/CobraPython.cpp create mode 100644 lib/bindings/python/CobraPython.hpp diff --git a/lib/bindings/python/CMakeLists.txt b/lib/bindings/python/CMakeLists.txt index bfe6d75..2dd361e 100644 --- a/lib/bindings/python/CMakeLists.txt +++ b/lib/bindings/python/CMakeLists.txt @@ -16,6 +16,13 @@ if (nanobind_ROOT AND NOT nanobind_DIR) endif() find_package(nanobind CONFIG REQUIRED) -nanobind_add_module(cobra_mba PythonInterface.cpp PythonInterface.hpp) +nanobind_add_module( + cobra_mba + PythonInterface.cpp + PythonInterface.hpp + CobraPython.cpp + CobraPython.hpp +) +target_link_libraries(cobra_mba PRIVATE cobra-core) diff --git a/lib/bindings/python/CobraPython.cpp b/lib/bindings/python/CobraPython.cpp new file mode 100644 index 0000000..8c88790 --- /dev/null +++ b/lib/bindings/python/CobraPython.cpp @@ -0,0 +1,70 @@ +#include "CobraPython.hpp" + +#include +#include + +namespace cobra::py { + +PyExpr PyExpr::FromExprNode(const Expr &expr) { + PyExpr out; + out.kind = expr.kind; + out.constant_val = expr.constant_val; + out.var_index = expr.var_index; + out.children.reserve(expr.children.size()); + for (const auto &child : expr.children) { + out.children.push_back(FromExprNode(*child)); + } + return out; +} + +void PyExpr::RequireArity(const PyExpr &node, size_t expected, const char *label) { + if (node.children.size() != expected) { + throw std::runtime_error( + std::string("PyExpr ") + label + " expects " + std::to_string(expected) + + " child(ren)" + ); + } +} + +std::unique_ptr< Expr > PyExpr::ToExprNode(const PyExpr &node) { + switch (node.kind) { + case Expr::Kind::kConstant: + return Expr::Constant(node.constant_val); + case Expr::Kind::kVariable: + return Expr::Variable(node.var_index); + case Expr::Kind::kNot: + RequireArity(node, 1, "Not"); + return Expr::BitwiseNot(ToExprNode(node.children[0])); + case Expr::Kind::kNeg: + RequireArity(node, 1, "Neg"); + return Expr::Negate(ToExprNode(node.children[0])); + case Expr::Kind::kShr: + RequireArity(node, 1, "Shr"); + return Expr::LogicalShr(ToExprNode(node.children[0]), node.constant_val); + case Expr::Kind::kAdd: + RequireArity(node, 2, "Add"); + return Expr::Add(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + case Expr::Kind::kMul: + RequireArity(node, 2, "Mul"); + return Expr::Mul(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + case Expr::Kind::kAnd: + RequireArity(node, 2, "And"); + return Expr::BitwiseAnd(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + case Expr::Kind::kOr: + RequireArity(node, 2, "Or"); + return Expr::BitwiseOr(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + case Expr::Kind::kXor: + RequireArity(node, 2, "Xor"); + return Expr::BitwiseXor(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + } + throw std::runtime_error("PyExpr has unknown kind"); +} + +PyExprTree::PyExprTree(const std::string &s, uint32_t max_vars, uint32_t bitwidth) { + (void)s; + (void)max_vars; + (void)bitwidth; + throw std::runtime_error("PyExprTree is not implemented yet"); +} + +} // namespace cobra::py diff --git a/lib/bindings/python/CobraPython.hpp b/lib/bindings/python/CobraPython.hpp new file mode 100644 index 0000000..a0db8c0 --- /dev/null +++ b/lib/bindings/python/CobraPython.hpp @@ -0,0 +1,29 @@ +#include "cobra/core/Expr.h" + +namespace cobra::py { + // Python-facing copy of Expr. Owns a value tree so it can be edited safely. + struct PyExpr + { + cobra::Expr::Kind kind = Expr::Kind::kConstant; + uint64_t constant_val = 0; + uint32_t var_index = 0; + std::vector< PyExpr > children; + + std::unique_ptr< Expr > ToExpr() const { return ToExprNode(*this); } + + static PyExpr FromExprNode(const Expr &expr); + + private: + static void RequireArity(const PyExpr &node, size_t expected, const char *label); + + static std::unique_ptr< Expr > ToExprNode(const PyExpr &node); + }; + + struct PyExprTree + { + PyExpr root; + std::vector< std::string > vars; + + explicit PyExprTree(const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64); + }; +} // namespace cobra::py diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp index bd25804..c16aa94 100644 --- a/lib/bindings/python/PythonInterface.cpp +++ b/lib/bindings/python/PythonInterface.cpp @@ -1,7 +1,34 @@ #include +#include +#include + +#include +#include + +#include "CobraPython.hpp" + + +namespace nb = nanobind; +using namespace nb::literals; -int add(int a, int b) {return a + b;} NB_MODULE(cobra_mba, m) { - m.def("add", &add); + nb::enum_(m, "ExprKind") + .value("Constant", cobra::Expr::Kind::kConstant) + .value("Variable", cobra::Expr::Kind::kVariable) + .value("Add", cobra::Expr::Kind::kAdd) + .value("Mul", cobra::Expr::Kind::kMul) + .value("And", cobra::Expr::Kind::kAnd) + .value("Or", cobra::Expr::Kind::kOr) + .value("Xor", cobra::Expr::Kind::kXor) + .value("Not", cobra::Expr::Kind::kNot) + .value("Neg", cobra::Expr::Kind::kNeg) + .value("Shr", cobra::Expr::Kind::kShr); + nb::class_(m, "Expr") + .def_ro("kind", &cobra::py::PyExpr::kind) + .def_ro("constant_val", &cobra::py::PyExpr::constant_val) + .def_ro("var_index", &cobra::py::PyExpr::var_index) + .def_ro("children", &cobra::py::PyExpr::children); + nb::class_(m, "ExprTree") + .def(nb::init()); } From 308a715b5de8509984fab464a006d6b47c776454 Mon Sep 17 00:00:00 2001 From: Archie S Date: Sat, 11 Apr 2026 01:36:06 +0100 Subject: [PATCH 3/7] Adding expression parsing Piggy backing off the CLI's parser to avoid duplicated code --- lib/bindings/python/CMakeLists.txt | 2 ++ lib/bindings/python/CobraPython.cpp | 29 +++++++++++++++++++++---- lib/bindings/python/CobraPython.hpp | 15 ++++++++++++- lib/bindings/python/PythonInterface.cpp | 24 +++++++++++++------- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/lib/bindings/python/CMakeLists.txt b/lib/bindings/python/CMakeLists.txt index 2dd361e..403ea08 100644 --- a/lib/bindings/python/CMakeLists.txt +++ b/lib/bindings/python/CMakeLists.txt @@ -22,7 +22,9 @@ nanobind_add_module( PythonInterface.hpp CobraPython.cpp CobraPython.hpp + ${PROJECT_SOURCE_DIR}/tools/cobra-cli/ExprParser.cpp ) target_link_libraries(cobra_mba PRIVATE cobra-core) +target_include_directories(cobra_mba PRIVATE ${PROJECT_SOURCE_DIR}/tools/cobra-cli) diff --git a/lib/bindings/python/CobraPython.cpp b/lib/bindings/python/CobraPython.cpp index 8c88790..26e9331 100644 --- a/lib/bindings/python/CobraPython.cpp +++ b/lib/bindings/python/CobraPython.cpp @@ -1,4 +1,6 @@ #include "CobraPython.hpp" +#include "ExprParser.h" +#include "cobra/core/Classifier.h" #include #include @@ -61,10 +63,29 @@ std::unique_ptr< Expr > PyExpr::ToExprNode(const PyExpr &node) { } PyExprTree::PyExprTree(const std::string &s, uint32_t max_vars, uint32_t bitwidth) { - (void)s; - (void)max_vars; - (void)bitwidth; - throw std::runtime_error("PyExprTree is not implemented yet"); + auto parsed = cobra::ParseToAst(s, bitwidth); + if (!parsed.has_value()) { + throw std::runtime_error(parsed.error().message); + } + + auto &ast = parsed.value(); + if (ast.vars.size() > max_vars) { + throw std::runtime_error( + "expression has " + std::to_string(ast.vars.size()) + + " variables (max " + std::to_string(max_vars) + ")" + ); + } + + this->bitwidth = bitwidth; + + auto folded = cobra::FoldConstantBitwise(std::move(ast.expr), bitwidth); + root = PyExpr::FromExprNode(*folded); + vars = std::move(ast.vars); +} + +std::string PyExprTree::ToString() const { + auto expr = root.ToExpr(); + return cobra::Render(*expr, vars, bitwidth); } } // namespace cobra::py diff --git a/lib/bindings/python/CobraPython.hpp b/lib/bindings/python/CobraPython.hpp index a0db8c0..d430fc4 100644 --- a/lib/bindings/python/CobraPython.hpp +++ b/lib/bindings/python/CobraPython.hpp @@ -23,7 +23,20 @@ namespace cobra::py { { PyExpr root; std::vector< std::string > vars; + uint32_t bitwidth = 64; + + explicit PyExprTree( + const PyExpr &expr, std::vector< std::string > vars, uint32_t bitwidth = 64 + ) + : root(expr), vars(std::move(vars)), bitwidth(bitwidth) {} + explicit PyExprTree( + const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64 + ); + std::string ToString() const; + std::unique_ptr< Expr > ToExpr() const { return root.ToExpr(); } + void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } - explicit PyExprTree(const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64); }; + + } // namespace cobra::py diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp index c16aa94..af821ea 100644 --- a/lib/bindings/python/PythonInterface.cpp +++ b/lib/bindings/python/PythonInterface.cpp @@ -2,16 +2,11 @@ #include #include -#include -#include - #include "CobraPython.hpp" - namespace nb = nanobind; using namespace nb::literals; - NB_MODULE(cobra_mba, m) { nb::enum_(m, "ExprKind") .value("Constant", cobra::Expr::Kind::kConstant) @@ -24,11 +19,24 @@ NB_MODULE(cobra_mba, m) { .value("Not", cobra::Expr::Kind::kNot) .value("Neg", cobra::Expr::Kind::kNeg) .value("Shr", cobra::Expr::Kind::kShr); - nb::class_(m, "Expr") + nb::class_(m, "ExprNode") .def_ro("kind", &cobra::py::PyExpr::kind) .def_ro("constant_val", &cobra::py::PyExpr::constant_val) .def_ro("var_index", &cobra::py::PyExpr::var_index) .def_ro("children", &cobra::py::PyExpr::children); - nb::class_(m, "ExprTree") - .def(nb::init()); + nb::class_(m, "Expr") + .def(nb::init(), + "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64) + .def(nb::init, uint32_t>(), + "root"_a, "vars"_a, "bitwidth"_a = 64) + .def_static( + "parse", + [](const std::string &expr, uint32_t max_vars, uint32_t bitwidth) { + return cobra::py::PyExprTree(expr, max_vars, bitwidth); + }, + "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64 + ) + .def("__str__", &cobra::py::PyExprTree::ToString) + .def_rw("variables", &cobra::py::PyExprTree::vars) + .def_rw("root", &cobra::py::PyExprTree::root); } From 345c152ba8d424c2442c84121f57f9c279bd1095 Mon Sep 17 00:00:00 2001 From: Archie S Date: Sat, 11 Apr 2026 02:19:52 +0100 Subject: [PATCH 4/7] Formatting and imporved documentation --- lib/bindings/python/CobraPython.cpp | 165 +++++++++++++----------- lib/bindings/python/CobraPython.hpp | 14 +- lib/bindings/python/PythonInterface.cpp | 68 ++++++---- lib/bindings/python/PythonInterface.hpp | 0 4 files changed, 146 insertions(+), 101 deletions(-) delete mode 100644 lib/bindings/python/PythonInterface.hpp diff --git a/lib/bindings/python/CobraPython.cpp b/lib/bindings/python/CobraPython.cpp index 26e9331..ff05521 100644 --- a/lib/bindings/python/CobraPython.cpp +++ b/lib/bindings/python/CobraPython.cpp @@ -7,85 +7,106 @@ namespace cobra::py { -PyExpr PyExpr::FromExprNode(const Expr &expr) { - PyExpr out; - out.kind = expr.kind; - out.constant_val = expr.constant_val; - out.var_index = expr.var_index; - out.children.reserve(expr.children.size()); - for (const auto &child : expr.children) { - out.children.push_back(FromExprNode(*child)); + // converts a core Expr into a python facing PyExpr + // copys the entier tree to avoid lifetime issues due to the mutability of PyExpr + PyExpr PyExpr::FromExprNode(const Expr &expr) { + PyExpr out; + out.kind = expr.kind; + out.constant_val = expr.constant_val; + out.var_index = expr.var_index; + out.children.reserve(expr.children.size()); + for (const auto &child : expr.children) { + out.children.push_back(FromExprNode(*child)); + } + return out; } - return out; -} - -void PyExpr::RequireArity(const PyExpr &node, size_t expected, const char *label) { - if (node.children.size() != expected) { - throw std::runtime_error( - std::string("PyExpr ") + label + " expects " + std::to_string(expected) - + " child(ren)" - ); - } -} - -std::unique_ptr< Expr > PyExpr::ToExprNode(const PyExpr &node) { - switch (node.kind) { - case Expr::Kind::kConstant: - return Expr::Constant(node.constant_val); - case Expr::Kind::kVariable: - return Expr::Variable(node.var_index); - case Expr::Kind::kNot: - RequireArity(node, 1, "Not"); - return Expr::BitwiseNot(ToExprNode(node.children[0])); - case Expr::Kind::kNeg: - RequireArity(node, 1, "Neg"); - return Expr::Negate(ToExprNode(node.children[0])); - case Expr::Kind::kShr: - RequireArity(node, 1, "Shr"); - return Expr::LogicalShr(ToExprNode(node.children[0]), node.constant_val); - case Expr::Kind::kAdd: - RequireArity(node, 2, "Add"); - return Expr::Add(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - case Expr::Kind::kMul: - RequireArity(node, 2, "Mul"); - return Expr::Mul(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - case Expr::Kind::kAnd: - RequireArity(node, 2, "And"); - return Expr::BitwiseAnd(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - case Expr::Kind::kOr: - RequireArity(node, 2, "Or"); - return Expr::BitwiseOr(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - case Expr::Kind::kXor: - RequireArity(node, 2, "Xor"); - return Expr::BitwiseXor(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - } - throw std::runtime_error("PyExpr has unknown kind"); -} -PyExprTree::PyExprTree(const std::string &s, uint32_t max_vars, uint32_t bitwidth) { - auto parsed = cobra::ParseToAst(s, bitwidth); - if (!parsed.has_value()) { - throw std::runtime_error(parsed.error().message); + // require given no. subnexpressions + void PyExpr::RequireArity(const PyExpr &node, size_t expected, const char *label) { + if (node.children.size() != expected) { + throw std::runtime_error( + std::string("PyExpr ") + label + " expects " + std::to_string(expected) + + " child(ren)" + ); + } } - auto &ast = parsed.value(); - if (ast.vars.size() > max_vars) { - throw std::runtime_error( - "expression has " + std::to_string(ast.vars.size()) - + " variables (max " + std::to_string(max_vars) + ")" - ); + // Converts a PyExpr back into a core Expr + // TODO: Maybe force tailcall to prevent stack overflows on large linear expressions + std::unique_ptr< Expr > PyExpr::ToExprNode(const PyExpr &node) { + switch (node.kind) { + case Expr::Kind::kConstant: + return Expr::Constant(node.constant_val); + + case Expr::Kind::kVariable: + return Expr::Variable(node.var_index); + + case Expr::Kind::kNot: + RequireArity(node, 1, "Not"); + return Expr::BitwiseNot(ToExprNode(node.children[0])); + + case Expr::Kind::kNeg: + RequireArity(node, 1, "Neg"); + return Expr::Negate(ToExprNode(node.children[0])); + + case Expr::Kind::kShr: + RequireArity(node, 1, "Shr"); + return Expr::LogicalShr(ToExprNode(node.children[0]), node.constant_val); + + case Expr::Kind::kAdd: + RequireArity(node, 2, "Add"); + return Expr::Add(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + + case Expr::Kind::kMul: + RequireArity(node, 2, "Mul"); + return Expr::Mul(ToExprNode(node.children[0]), ToExprNode(node.children[1])); + + case Expr::Kind::kAnd: + RequireArity(node, 2, "And"); + return Expr::BitwiseAnd( + ToExprNode(node.children[0]), ToExprNode(node.children[1]) + ); + + case Expr::Kind::kOr: + RequireArity(node, 2, "Or"); + return Expr::BitwiseOr( + ToExprNode(node.children[0]), ToExprNode(node.children[1]) + ); + + case Expr::Kind::kXor: + RequireArity(node, 2, "Xor"); + return Expr::BitwiseXor( + ToExprNode(node.children[0]), ToExprNode(node.children[1]) + ); + } + throw std::runtime_error("PyExpr has unknown kind"); } - this->bitwidth = bitwidth; + // Parses an expression from a string. Uses the same parser as the cobra-cli tool. + // default params declared in header + PyExprTree::PyExprTree( + const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64 + ) { + auto parsed = cobra::ParseToAst(s, bitwidth); + if (!parsed.has_value()) { throw std::runtime_error(parsed.error().message); } - auto folded = cobra::FoldConstantBitwise(std::move(ast.expr), bitwidth); - root = PyExpr::FromExprNode(*folded); - vars = std::move(ast.vars); -} + auto &ast = parsed.value(); + if (ast.vars.size() > max_vars) { + throw std::runtime_error( + "expression has " + std::to_string(ast.vars.size()) + " variables (max " + + std::to_string(max_vars) + ")" + ); + } -std::string PyExprTree::ToString() const { - auto expr = root.ToExpr(); - return cobra::Render(*expr, vars, bitwidth); -} + this->bitwidth = bitwidth; + auto folded = cobra::FoldConstantBitwise(std::move(ast.expr), bitwidth); + this->root = PyExpr::FromExprNode(*folded); + this->vars = std::move(ast.vars); + } + + std::string PyExprTree::ToString() const { + auto expr = root.ToExpr(); + return cobra::Render(*expr, vars, bitwidth); + } } // namespace cobra::py diff --git a/lib/bindings/python/CobraPython.hpp b/lib/bindings/python/CobraPython.hpp index d430fc4..4c6e718 100644 --- a/lib/bindings/python/CobraPython.hpp +++ b/lib/bindings/python/CobraPython.hpp @@ -2,6 +2,7 @@ namespace cobra::py { // Python-facing copy of Expr. Owns a value tree so it can be edited safely. + // We will only have users interact with this if they want to modify the expression tree struct PyExpr { cobra::Expr::Kind kind = Expr::Kind::kConstant; @@ -19,6 +20,10 @@ namespace cobra::py { static std::unique_ptr< Expr > ToExprNode(const PyExpr &node); }; + // This is the struct the python bindings will mostly interact with as it will keep track of + // the variable name and bitwidth information. IMO best solution as it lets the libary user + // ignore the fact that the vars are internally represted as indices not strings (unless + // they want to modify it) struct PyExprTree { PyExpr root; @@ -29,14 +34,13 @@ namespace cobra::py { const PyExpr &expr, std::vector< std::string > vars, uint32_t bitwidth = 64 ) : root(expr), vars(std::move(vars)), bitwidth(bitwidth) {} - explicit PyExprTree( - const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64 - ); + + explicit PyExprTree(const std::string &s, uint32_t max_vars, uint32_t bitwidth); std::string ToString() const; + std::unique_ptr< Expr > ToExpr() const { return root.ToExpr(); } - void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } + void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } }; - } // namespace cobra::py diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp index af821ea..0161b86 100644 --- a/lib/bindings/python/PythonInterface.cpp +++ b/lib/bindings/python/PythonInterface.cpp @@ -8,35 +8,55 @@ namespace nb = nanobind; using namespace nb::literals; NB_MODULE(cobra_mba, m) { - nb::enum_(m, "ExprKind") - .value("Constant", cobra::Expr::Kind::kConstant) - .value("Variable", cobra::Expr::Kind::kVariable) - .value("Add", cobra::Expr::Kind::kAdd) - .value("Mul", cobra::Expr::Kind::kMul) - .value("And", cobra::Expr::Kind::kAnd) - .value("Or", cobra::Expr::Kind::kOr) - .value("Xor", cobra::Expr::Kind::kXor) - .value("Not", cobra::Expr::Kind::kNot) - .value("Neg", cobra::Expr::Kind::kNeg) - .value("Shr", cobra::Expr::Kind::kShr); - nb::class_(m, "ExprNode") - .def_ro("kind", &cobra::py::PyExpr::kind) - .def_ro("constant_val", &cobra::py::PyExpr::constant_val) - .def_ro("var_index", &cobra::py::PyExpr::var_index) - .def_ro("children", &cobra::py::PyExpr::children); - nb::class_(m, "Expr") - .def(nb::init(), - "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64) + nb::enum_< cobra::Expr::Kind >(m, "ExprKind") + .value("Constant", cobra::Expr::Kind::kConstant) + .value("Variablee", cobra::Expr::Kind::kVariable) + .value("Add", cobra::Expr::Kind::kAdd) + .value("Mul", cobra::Expr::Kind::kMul) + .value("And", cobra::Expr::Kind::kAnd) + .value("Or", cobra::Expr::Kind::kOr) + .value("Xor", cobra::Expr::Kind::kXor) + .value("Not", cobra::Expr::Kind::kNot) + .value("Neg", cobra::Expr::Kind::kNeg) + .value("Shr", cobra::Expr::Kind::kShr); + + nb::class_< cobra::py::PyExpr >(m, "ExprNode") + .def_ro("kind", &cobra::py::PyExpr::kind, "The type of the expression node") + .def_ro( + "constant_val", &cobra::py::PyExpr::constant_val, + "If a node is a constant, this will hold its value, undefined otherwise" + ) + .def_ro( + "var_index", &cobra::py::PyExpr::var_index, + "The variables no. true name can be recoved by using it as an index into the var " + "list, undefined if the node is not a variable" + ) + .def_ro( + "children", &cobra::py::PyExpr::children, + "List of child nodes of the expression node, such as the addends of an addition " + "node. Will be empty for constant and variable nodes" + ); + + nb::class_(m, "Expr") .def(nb::init, uint32_t>(), - "root"_a, "vars"_a, "bitwidth"_a = 64) - .def_static( + "root"_a, "vars"_a, "bitwidth"_a = 64, + "Create an expression tree from a root node, its variable names and bitwidth") + + .def(nb::init(), + "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64, + "Parse an expression from a string. Warning increasing max_vars past 16 may cause OOM errors") + + .def_static( // duplicate of the init method but with a nicer name "parse", [](const std::string &expr, uint32_t max_vars, uint32_t bitwidth) { return cobra::py::PyExprTree(expr, max_vars, bitwidth); }, - "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64 + "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64, + "Parse an expression from a string. Warning increasing max_vars past 16 may cause OOM errors" ) + .def("__str__", &cobra::py::PyExprTree::ToString) - .def_rw("variables", &cobra::py::PyExprTree::vars) - .def_rw("root", &cobra::py::PyExprTree::root); + .def_rw("variables", &cobra::py::PyExprTree::vars, "List of variable names in the expression") + .def_rw("root", &cobra::py::PyExprTree::root, "Root expression node") + .def_rw("bitwidth", &cobra::py::PyExprTree::bitwidth, "Bitwidth of the expression"); } diff --git a/lib/bindings/python/PythonInterface.hpp b/lib/bindings/python/PythonInterface.hpp deleted file mode 100644 index e69de29..0000000 From a163880812947104c4c3b1a46c334a1da1eeecb1 Mon Sep 17 00:00:00 2001 From: Archie S Date: Sat, 11 Apr 2026 03:41:15 +0100 Subject: [PATCH 5/7] Added Simplification bindings --- lib/bindings/python/CMakeLists.txt | 1 - lib/bindings/python/CobraPython.cpp | 54 +++++++++++++++++++++++++ lib/bindings/python/CobraPython.hpp | 2 + lib/bindings/python/PythonInterface.cpp | 11 ++++- 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/bindings/python/CMakeLists.txt b/lib/bindings/python/CMakeLists.txt index 403ea08..cb68f17 100644 --- a/lib/bindings/python/CMakeLists.txt +++ b/lib/bindings/python/CMakeLists.txt @@ -19,7 +19,6 @@ find_package(nanobind CONFIG REQUIRED) nanobind_add_module( cobra_mba PythonInterface.cpp - PythonInterface.hpp CobraPython.cpp CobraPython.hpp ${PROJECT_SOURCE_DIR}/tools/cobra-cli/ExprParser.cpp diff --git a/lib/bindings/python/CobraPython.cpp b/lib/bindings/python/CobraPython.cpp index ff05521..7d2f0ff 100644 --- a/lib/bindings/python/CobraPython.cpp +++ b/lib/bindings/python/CobraPython.cpp @@ -1,10 +1,27 @@ #include "CobraPython.hpp" #include "ExprParser.h" #include "cobra/core/Classifier.h" +#include "cobra/core/ExprUtils.h" +#include "cobra/core/SignatureChecker.h" +#include "cobra/core/Simplifier.h" #include #include +namespace { + std::vector< uint64_t > + EvaluateToSignature(const cobra::Expr &ast, uint32_t num_vars, uint32_t bitwidth) { + const size_t kLen = size_t{ 1 } << num_vars; + std::vector< uint64_t > sig(kLen); + for (size_t i = 0; i < kLen; ++i) { + std::vector< uint64_t > var_values(num_vars); + for (uint32_t v = 0; v < num_vars; ++v) { var_values[v] = (i >> v) & 1; } + sig[i] = cobra::EvalExpr(ast, var_values, bitwidth); + } + return sig; + } +} // namespace + namespace cobra::py { // converts a core Expr into a python facing PyExpr @@ -109,4 +126,41 @@ namespace cobra::py { auto expr = root.ToExpr(); return cobra::Render(*expr, vars, bitwidth); } + + void PyExprTree::Simplify(bool validate = false) { + auto expr = root.ToExpr(); + auto num_vars = static_cast< uint32_t >(vars.size()); + auto sig = EvaluateToSignature(*expr, num_vars, bitwidth); + + cobra::Options opts{ .bitwidth = bitwidth, .max_vars = num_vars, .spot_check = true }; + opts.evaluator = cobra::Evaluator::FromExpr( + *expr, bitwidth, cobra::EvaluatorTraceKind::kCliOriginalAst + ); + + auto result = cobra::Simplify(sig, vars, expr.get(), opts); + if (!result.has_value()) { throw std::runtime_error(result.error().message); } + + auto &outcome = result.value(); + if (outcome.kind == cobra::SimplifyOutcome::Kind::kError) { + throw std::runtime_error(outcome.diag.reason); + } + if (outcome.kind == cobra::SimplifyOutcome::Kind::kUnchangedUnsupported) { return; } + + if (validate) { + std::vector< uint32_t > var_map; + if (outcome.real_vars.size() < vars.size()) { + var_map = cobra::BuildVarSupport(vars, outcome.real_vars); + } + auto fw = cobra::FullWidthCheck(*expr, num_vars, *outcome.expr, var_map, bitwidth); + if (!fw.passed) { + throw std::runtime_error( + "CoB result is only correct on {0,1} inputs (polynomial target)" + ); + } + } + + this->root = PyExpr::FromExprNode(*outcome.expr); + this->vars = std::move(outcome.real_vars); + } + } // namespace cobra::py diff --git a/lib/bindings/python/CobraPython.hpp b/lib/bindings/python/CobraPython.hpp index 4c6e718..ddd013e 100644 --- a/lib/bindings/python/CobraPython.hpp +++ b/lib/bindings/python/CobraPython.hpp @@ -41,6 +41,8 @@ namespace cobra::py { std::unique_ptr< Expr > ToExpr() const { return root.ToExpr(); } void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } + + void Simplify(bool validate); }; } // namespace cobra::py diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp index 0161b86..b62cf24 100644 --- a/lib/bindings/python/PythonInterface.cpp +++ b/lib/bindings/python/PythonInterface.cpp @@ -1,3 +1,6 @@ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored \ + "-Wsign-conversion" // TODO: fix the bindings to have runtime erros for #include #include #include @@ -10,7 +13,7 @@ using namespace nb::literals; NB_MODULE(cobra_mba, m) { nb::enum_< cobra::Expr::Kind >(m, "ExprKind") .value("Constant", cobra::Expr::Kind::kConstant) - .value("Variablee", cobra::Expr::Kind::kVariable) + .value("Variable", cobra::Expr::Kind::kVariable) .value("Add", cobra::Expr::Kind::kAdd) .value("Mul", cobra::Expr::Kind::kMul) .value("And", cobra::Expr::Kind::kAnd) @@ -56,7 +59,13 @@ NB_MODULE(cobra_mba, m) { ) .def("__str__", &cobra::py::PyExprTree::ToString) + .def( + "simplify", &cobra::py::PyExprTree::Simplify, "validate"_a = false, + "Simplify the expression in place. When validate=true, run full-width checks" + ) .def_rw("variables", &cobra::py::PyExprTree::vars, "List of variable names in the expression") .def_rw("root", &cobra::py::PyExprTree::root, "Root expression node") .def_rw("bitwidth", &cobra::py::PyExprTree::bitwidth, "Bitwidth of the expression"); } + +#pragma GCC diagnostic pop From 63ada3761b9306cb733e57432652ab2bdd798b7c Mon Sep 17 00:00:00 2001 From: Archie S Date: Sat, 11 Apr 2026 13:37:46 +0100 Subject: [PATCH 6/7] Corrected formatter used --- lib/bindings/python/CobraPython.cpp | 294 ++++++++++++------------ lib/bindings/python/CobraPython.hpp | 66 +++--- lib/bindings/python/PythonInterface.cpp | 104 +++++---- 3 files changed, 233 insertions(+), 231 deletions(-) diff --git a/lib/bindings/python/CobraPython.cpp b/lib/bindings/python/CobraPython.cpp index 7d2f0ff..c97dbd1 100644 --- a/lib/bindings/python/CobraPython.cpp +++ b/lib/bindings/python/CobraPython.cpp @@ -9,158 +9,158 @@ #include namespace { - std::vector< uint64_t > - EvaluateToSignature(const cobra::Expr &ast, uint32_t num_vars, uint32_t bitwidth) { - const size_t kLen = size_t{ 1 } << num_vars; - std::vector< uint64_t > sig(kLen); - for (size_t i = 0; i < kLen; ++i) { - std::vector< uint64_t > var_values(num_vars); - for (uint32_t v = 0; v < num_vars; ++v) { var_values[v] = (i >> v) & 1; } - sig[i] = cobra::EvalExpr(ast, var_values, bitwidth); - } - return sig; - } +std::vector EvaluateToSignature(const cobra::Expr &ast, + uint32_t num_vars, + uint32_t bitwidth) { + const size_t kLen = size_t{1} << num_vars; + std::vector sig(kLen); + for (size_t i = 0; i < kLen; ++i) { + std::vector var_values(num_vars); + for (uint32_t v = 0; v < num_vars; ++v) + var_values[v] = (i >> v) & 1; + sig[i] = cobra::EvalExpr(ast, var_values, bitwidth); + } + return sig; +} } // namespace namespace cobra::py { - // converts a core Expr into a python facing PyExpr - // copys the entier tree to avoid lifetime issues due to the mutability of PyExpr - PyExpr PyExpr::FromExprNode(const Expr &expr) { - PyExpr out; - out.kind = expr.kind; - out.constant_val = expr.constant_val; - out.var_index = expr.var_index; - out.children.reserve(expr.children.size()); - for (const auto &child : expr.children) { - out.children.push_back(FromExprNode(*child)); - } - return out; - } - - // require given no. subnexpressions - void PyExpr::RequireArity(const PyExpr &node, size_t expected, const char *label) { - if (node.children.size() != expected) { - throw std::runtime_error( - std::string("PyExpr ") + label + " expects " + std::to_string(expected) - + " child(ren)" - ); - } - } - - // Converts a PyExpr back into a core Expr - // TODO: Maybe force tailcall to prevent stack overflows on large linear expressions - std::unique_ptr< Expr > PyExpr::ToExprNode(const PyExpr &node) { - switch (node.kind) { - case Expr::Kind::kConstant: - return Expr::Constant(node.constant_val); - - case Expr::Kind::kVariable: - return Expr::Variable(node.var_index); - - case Expr::Kind::kNot: - RequireArity(node, 1, "Not"); - return Expr::BitwiseNot(ToExprNode(node.children[0])); - - case Expr::Kind::kNeg: - RequireArity(node, 1, "Neg"); - return Expr::Negate(ToExprNode(node.children[0])); - - case Expr::Kind::kShr: - RequireArity(node, 1, "Shr"); - return Expr::LogicalShr(ToExprNode(node.children[0]), node.constant_val); - - case Expr::Kind::kAdd: - RequireArity(node, 2, "Add"); - return Expr::Add(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - - case Expr::Kind::kMul: - RequireArity(node, 2, "Mul"); - return Expr::Mul(ToExprNode(node.children[0]), ToExprNode(node.children[1])); - - case Expr::Kind::kAnd: - RequireArity(node, 2, "And"); - return Expr::BitwiseAnd( - ToExprNode(node.children[0]), ToExprNode(node.children[1]) - ); - - case Expr::Kind::kOr: - RequireArity(node, 2, "Or"); - return Expr::BitwiseOr( - ToExprNode(node.children[0]), ToExprNode(node.children[1]) - ); - - case Expr::Kind::kXor: - RequireArity(node, 2, "Xor"); - return Expr::BitwiseXor( - ToExprNode(node.children[0]), ToExprNode(node.children[1]) - ); - } - throw std::runtime_error("PyExpr has unknown kind"); +// converts a core Expr into a python facing PyExpr +// copys the entier tree to avoid lifetime issues due to the mutability of +// PyExpr +PyExpr PyExpr::FromExprNode(const Expr &expr) { + PyExpr out; + out.kind = expr.kind; + out.constant_val = expr.constant_val; + out.var_index = expr.var_index; + out.children.reserve(expr.children.size()); + for (const auto &child : expr.children) + out.children.push_back(FromExprNode(*child)); + return out; +} + +// require given no. subnexpressions +void PyExpr::RequireArity(const PyExpr &node, size_t expected, + const char *label) { + if (node.children.size() != expected) { + throw std::runtime_error(std::string("PyExpr ") + label + " expects " + + std::to_string(expected) + " child(ren)"); + } +} + +// Converts a PyExpr back into a core Expr +// TODO: Maybe force tailcall to prevent stack overflows on large linear +// expressions +std::unique_ptr PyExpr::ToExprNode(const PyExpr &node) { + switch (node.kind) { + case Expr::Kind::kConstant: + return Expr::Constant(node.constant_val); + + case Expr::Kind::kVariable: + return Expr::Variable(node.var_index); + + case Expr::Kind::kNot: + RequireArity(node, 1, "Not"); + return Expr::BitwiseNot(ToExprNode(node.children[0])); + + case Expr::Kind::kNeg: + RequireArity(node, 1, "Neg"); + return Expr::Negate(ToExprNode(node.children[0])); + + case Expr::Kind::kShr: + RequireArity(node, 1, "Shr"); + return Expr::LogicalShr(ToExprNode(node.children[0]), node.constant_val); + + case Expr::Kind::kAdd: + RequireArity(node, 2, "Add"); + return Expr::Add(ToExprNode(node.children[0]), + ToExprNode(node.children[1])); + + case Expr::Kind::kMul: + RequireArity(node, 2, "Mul"); + return Expr::Mul(ToExprNode(node.children[0]), + ToExprNode(node.children[1])); + + case Expr::Kind::kAnd: + RequireArity(node, 2, "And"); + return Expr::BitwiseAnd(ToExprNode(node.children[0]), + ToExprNode(node.children[1])); + + case Expr::Kind::kOr: + RequireArity(node, 2, "Or"); + return Expr::BitwiseOr(ToExprNode(node.children[0]), + ToExprNode(node.children[1])); + + case Expr::Kind::kXor: + RequireArity(node, 2, "Xor"); + return Expr::BitwiseXor(ToExprNode(node.children[0]), + ToExprNode(node.children[1])); + } + throw std::runtime_error("PyExpr has unknown kind"); +} + +// Parses an expression from a string. Uses the same parser as the cobra-cli +// tool. default params declared in header +PyExprTree::PyExprTree(const std::string &s, uint32_t max_vars = 16, + uint32_t bitwidth = 64) { + auto parsed = cobra::ParseToAst(s, bitwidth); + if (!parsed.has_value()) + throw std::runtime_error(parsed.error().message); + + auto &ast = parsed.value(); + if (ast.vars.size() > max_vars) { + throw std::runtime_error( + "expression has " + std::to_string(ast.vars.size()) + + " variables (max " + std::to_string(max_vars) + ")"); + } + + this->bitwidth = bitwidth; + + auto folded = cobra::FoldConstantBitwise(std::move(ast.expr), bitwidth); + this->root = PyExpr::FromExprNode(*folded); + this->vars = std::move(ast.vars); +} + +std::string PyExprTree::ToString() const { + auto expr = root.ToExpr(); + return cobra::Render(*expr, vars, bitwidth); +} + +void PyExprTree::Simplify(bool validate = false) { + auto expr = root.ToExpr(); + auto num_vars = static_cast(vars.size()); + auto sig = EvaluateToSignature(*expr, num_vars, bitwidth); + + cobra::Options opts{ + .bitwidth = bitwidth, .max_vars = num_vars, .spot_check = true}; + opts.evaluator = cobra::Evaluator::FromExpr( + *expr, bitwidth, cobra::EvaluatorTraceKind::kCliOriginalAst); + + auto result = cobra::Simplify(sig, vars, expr.get(), opts); + if (!result.has_value()) + throw std::runtime_error(result.error().message); + + auto &outcome = result.value(); + if (outcome.kind == cobra::SimplifyOutcome::Kind::kError) + throw std::runtime_error(outcome.diag.reason); + if (outcome.kind == cobra::SimplifyOutcome::Kind::kUnchangedUnsupported) + return; + + if (validate) { + std::vector var_map; + if (outcome.real_vars.size() < vars.size()) + var_map = cobra::BuildVarSupport(vars, outcome.real_vars); + auto fw = cobra::FullWidthCheck(*expr, num_vars, *outcome.expr, var_map, + bitwidth); + if (!fw.passed) { + throw std::runtime_error( + "CoB result is only correct on {0,1} inputs (polynomial target)"); } + } - // Parses an expression from a string. Uses the same parser as the cobra-cli tool. - // default params declared in header - PyExprTree::PyExprTree( - const std::string &s, uint32_t max_vars = 16, uint32_t bitwidth = 64 - ) { - auto parsed = cobra::ParseToAst(s, bitwidth); - if (!parsed.has_value()) { throw std::runtime_error(parsed.error().message); } - - auto &ast = parsed.value(); - if (ast.vars.size() > max_vars) { - throw std::runtime_error( - "expression has " + std::to_string(ast.vars.size()) + " variables (max " - + std::to_string(max_vars) + ")" - ); - } - - this->bitwidth = bitwidth; - - auto folded = cobra::FoldConstantBitwise(std::move(ast.expr), bitwidth); - this->root = PyExpr::FromExprNode(*folded); - this->vars = std::move(ast.vars); - } - - std::string PyExprTree::ToString() const { - auto expr = root.ToExpr(); - return cobra::Render(*expr, vars, bitwidth); - } - - void PyExprTree::Simplify(bool validate = false) { - auto expr = root.ToExpr(); - auto num_vars = static_cast< uint32_t >(vars.size()); - auto sig = EvaluateToSignature(*expr, num_vars, bitwidth); - - cobra::Options opts{ .bitwidth = bitwidth, .max_vars = num_vars, .spot_check = true }; - opts.evaluator = cobra::Evaluator::FromExpr( - *expr, bitwidth, cobra::EvaluatorTraceKind::kCliOriginalAst - ); - - auto result = cobra::Simplify(sig, vars, expr.get(), opts); - if (!result.has_value()) { throw std::runtime_error(result.error().message); } - - auto &outcome = result.value(); - if (outcome.kind == cobra::SimplifyOutcome::Kind::kError) { - throw std::runtime_error(outcome.diag.reason); - } - if (outcome.kind == cobra::SimplifyOutcome::Kind::kUnchangedUnsupported) { return; } - - if (validate) { - std::vector< uint32_t > var_map; - if (outcome.real_vars.size() < vars.size()) { - var_map = cobra::BuildVarSupport(vars, outcome.real_vars); - } - auto fw = cobra::FullWidthCheck(*expr, num_vars, *outcome.expr, var_map, bitwidth); - if (!fw.passed) { - throw std::runtime_error( - "CoB result is only correct on {0,1} inputs (polynomial target)" - ); - } - } - - this->root = PyExpr::FromExprNode(*outcome.expr); - this->vars = std::move(outcome.real_vars); - } + this->root = PyExpr::FromExprNode(*outcome.expr); + this->vars = std::move(outcome.real_vars); +} } // namespace cobra::py diff --git a/lib/bindings/python/CobraPython.hpp b/lib/bindings/python/CobraPython.hpp index ddd013e..37d1dac 100644 --- a/lib/bindings/python/CobraPython.hpp +++ b/lib/bindings/python/CobraPython.hpp @@ -1,48 +1,48 @@ #include "cobra/core/Expr.h" namespace cobra::py { - // Python-facing copy of Expr. Owns a value tree so it can be edited safely. - // We will only have users interact with this if they want to modify the expression tree - struct PyExpr - { - cobra::Expr::Kind kind = Expr::Kind::kConstant; - uint64_t constant_val = 0; - uint32_t var_index = 0; - std::vector< PyExpr > children; +// Python-facing copy of Expr. Owns a value tree so it can be edited safely. +// We will only have users interact with this if they want to modify the +// expression tree +struct PyExpr { + cobra::Expr::Kind kind = Expr::Kind::kConstant; + uint64_t constant_val = 0; + uint32_t var_index = 0; + std::vector children; - std::unique_ptr< Expr > ToExpr() const { return ToExprNode(*this); } + std::unique_ptr ToExpr() const { return ToExprNode(*this); } - static PyExpr FromExprNode(const Expr &expr); + static PyExpr FromExprNode(const Expr &expr); - private: - static void RequireArity(const PyExpr &node, size_t expected, const char *label); +private: + static void RequireArity(const PyExpr &node, size_t expected, + const char *label); - static std::unique_ptr< Expr > ToExprNode(const PyExpr &node); - }; + static std::unique_ptr ToExprNode(const PyExpr &node); +}; - // This is the struct the python bindings will mostly interact with as it will keep track of - // the variable name and bitwidth information. IMO best solution as it lets the libary user - // ignore the fact that the vars are internally represted as indices not strings (unless - // they want to modify it) - struct PyExprTree - { - PyExpr root; - std::vector< std::string > vars; - uint32_t bitwidth = 64; +// This is the struct the python bindings will mostly interact with as it will +// keep track of the variable name and bitwidth information. IMO best solution +// as it lets the libary user ignore the fact that the vars are internally +// represted as indices not strings (unless they want to modify it) +struct PyExprTree { + PyExpr root; + std::vector vars; + uint32_t bitwidth = 64; - explicit PyExprTree( - const PyExpr &expr, std::vector< std::string > vars, uint32_t bitwidth = 64 - ) - : root(expr), vars(std::move(vars)), bitwidth(bitwidth) {} + explicit PyExprTree(const PyExpr &expr, std::vector vars, + uint32_t bitwidth = 64) + : root(expr), vars(std::move(vars)), bitwidth(bitwidth) {} - explicit PyExprTree(const std::string &s, uint32_t max_vars, uint32_t bitwidth); - std::string ToString() const; + explicit PyExprTree(const std::string &s, uint32_t max_vars, + uint32_t bitwidth); + std::string ToString() const; - std::unique_ptr< Expr > ToExpr() const { return root.ToExpr(); } + std::unique_ptr ToExpr() const { return root.ToExpr(); } - void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } + void UpdateExpr(const Expr &expr) { root = PyExpr::FromExprNode(expr); } - void Simplify(bool validate); - }; + void Simplify(bool validate); +}; } // namespace cobra::py diff --git a/lib/bindings/python/PythonInterface.cpp b/lib/bindings/python/PythonInterface.cpp index b62cf24..c17af7d 100644 --- a/lib/bindings/python/PythonInterface.cpp +++ b/lib/bindings/python/PythonInterface.cpp @@ -1,5 +1,5 @@ #pragma GCC diagnostic push -#pragma GCC diagnostic ignored \ +#pragma GCC diagnostic ignored \ "-Wsign-conversion" // TODO: fix the bindings to have runtime erros for #include #include @@ -11,61 +11,63 @@ namespace nb = nanobind; using namespace nb::literals; NB_MODULE(cobra_mba, m) { - nb::enum_< cobra::Expr::Kind >(m, "ExprKind") - .value("Constant", cobra::Expr::Kind::kConstant) - .value("Variable", cobra::Expr::Kind::kVariable) - .value("Add", cobra::Expr::Kind::kAdd) - .value("Mul", cobra::Expr::Kind::kMul) - .value("And", cobra::Expr::Kind::kAnd) - .value("Or", cobra::Expr::Kind::kOr) - .value("Xor", cobra::Expr::Kind::kXor) - .value("Not", cobra::Expr::Kind::kNot) - .value("Neg", cobra::Expr::Kind::kNeg) - .value("Shr", cobra::Expr::Kind::kShr); + nb::enum_(m, "ExprKind") + .value("Constant", cobra::Expr::Kind::kConstant) + .value("Variable", cobra::Expr::Kind::kVariable) + .value("Add", cobra::Expr::Kind::kAdd) + .value("Mul", cobra::Expr::Kind::kMul) + .value("And", cobra::Expr::Kind::kAnd) + .value("Or", cobra::Expr::Kind::kOr) + .value("Xor", cobra::Expr::Kind::kXor) + .value("Not", cobra::Expr::Kind::kNot) + .value("Neg", cobra::Expr::Kind::kNeg) + .value("Shr", cobra::Expr::Kind::kShr); - nb::class_< cobra::py::PyExpr >(m, "ExprNode") - .def_ro("kind", &cobra::py::PyExpr::kind, "The type of the expression node") - .def_ro( - "constant_val", &cobra::py::PyExpr::constant_val, - "If a node is a constant, this will hold its value, undefined otherwise" - ) - .def_ro( - "var_index", &cobra::py::PyExpr::var_index, - "The variables no. true name can be recoved by using it as an index into the var " - "list, undefined if the node is not a variable" - ) - .def_ro( - "children", &cobra::py::PyExpr::children, - "List of child nodes of the expression node, such as the addends of an addition " - "node. Will be empty for constant and variable nodes" - ); + nb::class_(m, "ExprNode") + .def_ro("kind", &cobra::py::PyExpr::kind, + "The type of the expression node") + .def_ro("constant_val", &cobra::py::PyExpr::constant_val, + "If a node is a constant, this will hold its value, undefined " + "otherwise") + .def_ro("var_index", &cobra::py::PyExpr::var_index, + "The variables no. true name can be recoved by using it as an " + "index into the var " + "list, undefined if the node is not a variable") + .def_ro("children", &cobra::py::PyExpr::children, + "List of child nodes of the expression node, such as the addends " + "of an addition " + "node. Will be empty for constant and variable nodes"); - nb::class_(m, "Expr") - .def(nb::init, uint32_t>(), - "root"_a, "vars"_a, "bitwidth"_a = 64, - "Create an expression tree from a root node, its variable names and bitwidth") + nb::class_(m, "Expr") + .def(nb::init, + uint32_t>(), + "root"_a, "vars"_a, "bitwidth"_a = 64, + "Create an expression tree from a root node, its variable names and " + "bitwidth") - .def(nb::init(), - "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64, - "Parse an expression from a string. Warning increasing max_vars past 16 may cause OOM errors") + .def(nb::init(), "expr"_a, + "max_vars"_a = 16, "bitwidth"_a = 64, + "Parse an expression from a string. Warning increasing max_vars " + "past 16 may cause OOM errors") - .def_static( // duplicate of the init method but with a nicer name - "parse", - [](const std::string &expr, uint32_t max_vars, uint32_t bitwidth) { - return cobra::py::PyExprTree(expr, max_vars, bitwidth); - }, - "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64, - "Parse an expression from a string. Warning increasing max_vars past 16 may cause OOM errors" - ) + .def_static( // duplicate of the init method but with a nicer name + "parse", + [](const std::string &expr, uint32_t max_vars, uint32_t bitwidth) { + return cobra::py::PyExprTree(expr, max_vars, bitwidth); + }, + "expr"_a, "max_vars"_a = 16, "bitwidth"_a = 64, + "Parse an expression from a string. Warning increasing max_vars past " + "16 may cause OOM errors") - .def("__str__", &cobra::py::PyExprTree::ToString) - .def( - "simplify", &cobra::py::PyExprTree::Simplify, "validate"_a = false, - "Simplify the expression in place. When validate=true, run full-width checks" - ) - .def_rw("variables", &cobra::py::PyExprTree::vars, "List of variable names in the expression") - .def_rw("root", &cobra::py::PyExprTree::root, "Root expression node") - .def_rw("bitwidth", &cobra::py::PyExprTree::bitwidth, "Bitwidth of the expression"); + .def("__str__", &cobra::py::PyExprTree::ToString) + .def("simplify", &cobra::py::PyExprTree::Simplify, "validate"_a = false, + "Simplify the expression in place. When validate=true, run " + "full-width checks") + .def_rw("variables", &cobra::py::PyExprTree::vars, + "List of variable names in the expression") + .def_rw("root", &cobra::py::PyExprTree::root, "Root expression node") + .def_rw("bitwidth", &cobra::py::PyExprTree::bitwidth, + "Bitwidth of the expression"); } #pragma GCC diagnostic pop From dafc5a6d5d122b53e098d9f32d4cfaff8ddec5f7 Mon Sep 17 00:00:00 2001 From: Archie S Date: Mon, 13 Apr 2026 21:49:24 +0100 Subject: [PATCH 7/7] Setup wheel generation --- CMakeLists.txt | 16 ++++++++- dependencies/CMakeLists.txt | 6 +++- dependencies/abseil.cmake | 1 + lib/bindings/python/CMakeLists.txt | 12 ++++++- lib/core/CMakeLists.txt | 2 +- pyproject.toml | 58 ++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 pyproject.toml diff --git a/CMakeLists.txt b/CMakeLists.txt index da2445a..1ac12e3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,16 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +# Prefer local dependency prefix if present (helps wheel builds find absl/hwy) +if(DEFINED COBRA_DEPENDENCY_PREFIX) + list(PREPEND CMAKE_PREFIX_PATH "${COBRA_DEPENDENCY_PREFIX}") +else() + set(_cobra_default_dep_prefix "${CMAKE_SOURCE_DIR}/build-deps/install") + if(EXISTS "${_cobra_default_dep_prefix}") + list(PREPEND CMAKE_PREFIX_PATH "${_cobra_default_dep_prefix}") + endif() +endif() + if (CMAKE_EXPORT_COMPILE_COMMANDS AND UNIX) set(_cobra_cc_src "${CMAKE_SOURCE_DIR}/compile_commands.json") set(_cobra_cc_bin "${CMAKE_BINARY_DIR}/compile_commands.json") @@ -124,7 +134,11 @@ install(DIRECTORY include/cobra # Collect installed targets for the package config export set set(_cobra_install_targets cobra-core cobra-cli) -if(Z3_FOUND) +if(DEFINED SKBUILD_PLATLIB_DIR) + # Wheel builds only need the extension module; skip installing the CLI. + set(_cobra_install_targets cobra-core) +endif() +if(Z3_FOUND AND NOT DEFINED SKBUILD_PLATLIB_DIR) list(APPEND _cobra_install_targets cobra-verify) endif() diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 1e998b0..8ebbcbe 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -7,6 +7,7 @@ # # Options: # USE_EXTERNAL_LLVM (default ON) - Use system LLVM vs build from source +# COBRA_BUILD_LLVM_PASS (default ON) - Prepare LLVM dependency for pass plugin # USE_EXTERNAL_ABSEIL (default OFF) - Use system abseil vs build from source # USE_EXTERNAL_HIGHWAY (default OFF) - Use system highway vs build from source # COBRA_BUILD_PYTHON_BINDINGS (default OFF) - Build nanobind for Python bindings @@ -20,6 +21,7 @@ cmake_minimum_required(VERSION 3.20) project(cobra-dependencies LANGUAGES C CXX) option(USE_EXTERNAL_LLVM "Use system LLVM instead of building from source" ON) +option(COBRA_BUILD_LLVM_PASS "Prepare LLVM dependency for the pass plugin" ON) option(USE_EXTERNAL_ABSEIL "Use system abseil instead of building from source" OFF) option(USE_EXTERNAL_HIGHWAY "Use system highway instead of building from source" OFF) option(COBRA_BUILD_PYTHON_BINDINGS "Build nanobind for Python bindings" OFF) @@ -33,7 +35,9 @@ include(superbuild.cmake) include(abseil.cmake) include(highway.cmake) -include(llvm.cmake) +if(COBRA_BUILD_LLVM_PASS) + include(llvm.cmake) +endif() if(COBRA_BUILD_PYTHON_BINDINGS) include(nanobind.cmake) diff --git a/dependencies/abseil.cmake b/dependencies/abseil.cmake index 48e5d72..c6c28ab 100644 --- a/dependencies/abseil.cmake +++ b/dependencies/abseil.cmake @@ -22,6 +22,7 @@ else() CMAKE_ARGS ${COBRA_COMMON_CMAKE_ARGS} -DABSL_BUILD_TESTING=OFF + -DABSL_ENABLE_INSTALL=ON -DABSL_PROPAGATE_CXX_STD=ON -DCMAKE_POSITION_INDEPENDENT_CODE=ON ) diff --git a/lib/bindings/python/CMakeLists.txt b/lib/bindings/python/CMakeLists.txt index cb68f17..a3d00e2 100644 --- a/lib/bindings/python/CMakeLists.txt +++ b/lib/bindings/python/CMakeLists.txt @@ -1,4 +1,7 @@ -find_package(Python 3.10 COMPONENTS Interpreter Development REQUIRED) +find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) +if(Python_VERSION VERSION_LESS 3.10) + message(FATAL_ERROR "Python 3.10+ is required for cobra_mba") +endif() if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose the type of build." FORCE) @@ -26,4 +29,11 @@ nanobind_add_module( target_link_libraries(cobra_mba PRIVATE cobra-core) target_include_directories(cobra_mba PRIVATE ${PROJECT_SOURCE_DIR}/tools/cobra-cli) +if (DEFINED SKBUILD_PLATLIB_DIR) + install(TARGETS cobra_mba + LIBRARY DESTINATION "${SKBUILD_PLATLIB_DIR}" + RUNTIME DESTINATION "${SKBUILD_PLATLIB_DIR}" + ) +endif() + diff --git a/lib/core/CMakeLists.txt b/lib/core/CMakeLists.txt index 48816bc..a6838b9 100644 --- a/lib/core/CMakeLists.txt +++ b/lib/core/CMakeLists.txt @@ -64,4 +64,4 @@ target_include_directories(cobra-core PUBLIC find_package(absl REQUIRED) find_package(hwy REQUIRED CONFIG) -target_link_libraries(cobra-core PUBLIC absl::flat_hash_map PRIVATE hwy::hwy) +target_link_libraries(cobra-core PUBLIC absl::flat_hash_map hwy::hwy) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c1db40e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,58 @@ +[build-system] +requires = [ + "scikit-build-core>=0.8", + "nanobind>=1.8", + "cmake>=3.20", + "ninja", +] +build-backend = "scikit_build_core.build" + +[project] +name = "cobra-mba" +version = "1.0.0" +description = "Python bindings for the CoBRA's MBA simplifier" +readme = "README.md" +requires-python = ">=3.10" +license = { file = "LICENSE" } +keywords = ["cobra", "mba", "bindings", "nanobind"] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: C++", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", +] + +[tool.scikit-build] +minimum-version = "0.8" +build-dir = "build/{wheel_tag}" +cmake.build-type = "Release" +cmake.targets = ["cobra_mba"] + +[tool.scikit-build.cmake.define] +COBRA_BUILD_PYTHON_BINDINGS = "ON" +COBRA_BUILD_LLVM_PASS = "OFF" +COBRA_BUILD_TESTS = "OFF" + +[tool.scikit-build.wheel] +packages = [] +license-files = ["LICENSE"] + +[tool.cibuildwheel] +build = "cp310-* cp311-* cp312-* cp313-*" +skip = "*-musllinux*" +build-verbosity = 1 +test-command = "python -c \"import cobra_mba; expr = cobra_mba.Expr('(a^b)+(a&b)'); expr.simplify(); assert str(expr) == 'a | b'\"" + +[tool.cibuildwheel.environment] +CMAKE_PREFIX_PATH = "{project}/build-deps/install" + +[tool.cibuildwheel.linux] +before-all = "rm -rf build build-deps && python -m pip install --upgrade pip cmake ninja && cmake -S dependencies -B build-deps -G Ninja -DCMAKE_BUILD_TYPE=Release -DCOBRA_BUILD_PYTHON_BINDINGS=ON -DCOBRA_BUILD_LLVM_PASS=OFF && cmake --build build-deps" + +#[tool.cibuildwheel.macos] # TODO: TEST THESE +#before-all = "rm -rf build build-deps && python -m pip install --upgrade pip cmake ninja && cmake -S dependencies -B build-deps -G Ninja -DCMAKE_BUILD_TYPE=Release -DCOBRA_BUILD_PYTHON_BINDINGS=ON -DCOBRA_BUILD_LLVM_PASS=OFF && cmake --build build-deps" +# +#[tool.cibuildwheel.windows] +#before-all = "if exist build rmdir /s /q build && if exist build-deps rmdir /s /q build-deps && python -m pip install --upgrade pip cmake && cmake -S dependencies -B build-deps -G \"Visual Studio 17 2022\" -A x64 -DCOBRA_BUILD_PYTHON_BINDINGS=ON -DCOBRA_BUILD_LLVM_PASS=OFF && cmake --build build-deps --config Release"