From 3bf4d2b53d5e63f66fbb0dd919cca43902ebbacb Mon Sep 17 00:00:00 2001 From: Alcides Fonseca Date: Thu, 18 Jun 2026 20:05:03 +0100 Subject: [PATCH 1/3] python: build a PyPI-publishable Pyodide (PEP 783) wheel (#9891) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary z3 already builds under Pyodide (there is a `pyodide.yml` workflow and an `IS_PYODIDE` path in `setup.py`), but that path uses `pyodide build` and produces a wheel tagged `emscripten__wasm32`, which is pinned to a single Pyodide release and rejected by PyPI — so today it's only usable as a CI artifact. [PEP 783](https://peps.python.org/pep-0783/) introduced the portable `pyemscripten__wasm32` platform tag that **PyPI accepts** and `micropip` can install at runtime. This makes `z3-solver` build that wheel via `cibuildwheel --platform pyodide`. ## Changes - **`setup.py`** — for the emscripten target, use the wheel platform tag that pyodide-build provides verbatim via `_PYTHON_HOST_PLATFORM` (e.g. `pyemscripten_2026_0_wasm32`) instead of reconstructing an `emscripten_*` tag. Falls back to the previous behaviour when that env var is absent. - **`setup.py` / `CMakeLists.txt` / `pyproject.toml`** — switch the Pyodide build from JS-based exceptions (`-fexceptions`, `-sDISABLE_EXCEPTION_CATCHING=0`) to **native wasm exception handling** (`-fwasm-exceptions -sSUPPORT_LONGJMP=wasm`), matching the ABI of the Pyodide 314 / emscripten 5 main module. With the old flags `libz3.so` imports `invoke_*` trampolines the runtime no longer provides, so the wheel builds but the first `Z3_mk_config` call fails at runtime with `Dynamic linking error: cannot resolve symbol invoke_vi`. - **`pyproject.toml`** — add `[tool.cibuildwheel]` / `[tool.pyodide.build]` so `cibuildwheel --platform pyodide` builds and tests the wheel (`cp314`). - **`.github/workflows/pyodide-pypi.yml`** (new) — build with cibuildwheel and publish to PyPI (trusted publishing) on tags. Existing `pyodide.yml` unchanged. ## Verification Built with `cibuildwheel 4.1.0` / `pyodide-build 0.35.0` / `emscripten 5.0.3`, CPython 3.14 / Pyodide 314: - Produces `z3_solver-4.17.0.0-py3-none-pyemscripten_2026_0_wasm32.whl`. - `z3test.py` passes in the Pyodide runtime (node + wasm32). - Installed via `micropip` and solves SMT problems both under node and in a browser (`sat` with a model, `unsat` for a contradiction). 🤖 Generated with [Claude Code](https://claude.com/claude-code) 🕵️‍♂️ Reviewed by [Alcides Fonseca](https://wiki.alcidesfonseca.com) Co-authored-by: Claude Opus 4.8 --- .github/workflows/pyodide-pypi.yml | 57 ++++++++++++++++++++++++++++++ CMakeLists.txt | 7 +++- src/api/python/pyproject.toml | 26 ++++++++++++++ src/api/python/setup.py | 28 +++++++++++++-- 4 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/pyodide-pypi.yml diff --git a/.github/workflows/pyodide-pypi.yml b/.github/workflows/pyodide-pypi.yml new file mode 100644 index 000000000..090145a65 --- /dev/null +++ b/.github/workflows/pyodide-pypi.yml @@ -0,0 +1,57 @@ +name: Pyodide Wheel (PyPI) + +# Builds a PEP 783 `pyemscripten_*_wasm32` wheel for z3-solver using cibuildwheel +# and publishes it to PyPI on tag pushes. Unlike the legacy pyodide.yml (which +# uses `pyodide build` and produces a Pyodide-version-locked emscripten_* wheel +# uploaded only as a CI artifact), this wheel is installable at runtime with +# micropip from PyPI. See src/api/python/pyproject.toml [tool.cibuildwheel]. + +on: + release: + types: [created] + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-pyodide: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + + - name: Build Pyodide wheel + uses: pypa/cibuildwheel@v4.1.0 + with: + # The Python bindings live in a subdirectory of the repo. + package-dir: src/api/python + env: + CIBW_PLATFORM: pyodide + # Exception/longjmp/bigint flags are declared in + # src/api/python/pyproject.toml ([tool.pyodide.build]) and combined + # with Pyodide's -fwasm-exceptions defaults. Don't set CFLAGS/CXXFLAGS + # here — a JS-EH -fexceptions value conflicts with the wasm-EH ABI. + + - name: Store Pyodide wheel + uses: actions/upload-artifact@v7 + with: + name: pyodide-wheel + path: wheelhouse/*.whl + + publish: + name: Publish to PyPI + runs-on: ubuntu-24.04 + if: startsWith(github.ref, 'refs/tags/') + needs: [build-pyodide] + environment: release + permissions: + id-token: write # trusted publishing (OIDC), no API token needed + steps: + - name: Download Pyodide wheel + uses: actions/download-artifact@v8 + with: + name: pyodide-wheel + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 1ff592e0e..491cb996b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -204,7 +204,12 @@ elseif (EMSCRIPTEN) "-Os" "-s ALLOW_MEMORY_GROWTH=1" "-s ASSERTIONS=0" - "-s DISABLE_EXCEPTION_CATCHING=0" + # Use native wasm exception handling + wasm longjmp to match the ABI of the + # Pyodide / modern-emscripten main module. The legacy JS-based EH (which the + # removed "-s DISABLE_EXCEPTION_CATCHING=0" selected) makes libz3 import + # invoke_* trampolines the Pyodide runtime no longer provides. + "-fwasm-exceptions" + "-s SUPPORT_LONGJMP=wasm" "-s ERROR_ON_UNDEFINED_SYMBOLS=1" ) endif() diff --git a/src/api/python/pyproject.toml b/src/api/python/pyproject.toml index 380744c87..243a85a94 100644 --- a/src/api/python/pyproject.toml +++ b/src/api/python/pyproject.toml @@ -1,3 +1,29 @@ [build-system] requires = ["setuptools>=70"] build-backend = "setuptools.build_meta" + +# --- Pyodide / WebAssembly (PEP 783) build configuration --------------------- +# Consumed by pyodide-build (invoked directly or via `cibuildwheel --platform +# pyodide`). These flags are forwarded to the emscripten toolchain that compiles +# libz3 to wasm32. setup.py's IS_PYODIDE branch appends the same -fexceptions +# flags defensively, but declaring them here is what cibuildwheel relies on. +# Pyodide 314 / emscripten 5 builds its main module with *native wasm* +# exception handling and wasm longjmp (see Pyodide's Makefile.envs). Side +# modules like libz3.so MUST match that ABI: building with the legacy +# `-fexceptions` (JS-based EH) makes libz3 import `invoke_*` trampolines that the +# Pyodide runtime no longer provides -> "cannot resolve symbol invoke_vi" at the +# first Z3 call. WASM_BIGINT is required because the Z3 C API passes 64-bit ints +# across the ctypes/JS boundary. +[tool.pyodide.build] +cflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm" +cxxflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm" +ldflags = "-fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_BIGINT" + +# --- cibuildwheel: produce a PyPI-publishable pyemscripten wheel ------------- +[tool.cibuildwheel] +# Pyodide 314 ships CPython 3.14; match the single ABI it targets. +build = "cp314-*" + +[tool.cibuildwheel.pyodide] +# z3test.py is the upstream smoke test; run it inside the Pyodide test venv. +test-command = "python {project}/z3test.py z3" diff --git a/src/api/python/setup.py b/src/api/python/setup.py index 116b3b570..029b5ea86 100644 --- a/src/api/python/setup.py +++ b/src/api/python/setup.py @@ -42,9 +42,16 @@ if RELEASE_DIR is None: BUILD_PLATFORM = "emscripten" BUILD_ARCH = "wasm32" BUILD_OS_VERSION = os.environ['_PYTHON_HOST_PLATFORM'].split('_')[1:-1] - build_env['CFLAGS'] = build_env.get('CFLAGS', '') + " -fexceptions" - build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + " -fexceptions" - build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + " -fexceptions" + # Match Pyodide's native-wasm exception/longjmp ABI (see Makefile.envs in + # Pyodide). The legacy JS-based "-fexceptions" makes libz3.so import + # invoke_* trampolines that the modern Pyodide runtime does not export, + # which surfaces as "cannot resolve symbol invoke_vi" on the first Z3 + # call. These mirror [tool.pyodide.build] in pyproject.toml so direct + # `pyodide build` invocations stay consistent with cibuildwheel. + _wasm_eh = " -fwasm-exceptions -sSUPPORT_LONGJMP=wasm" + build_env['CFLAGS'] = build_env.get('CFLAGS', '') + _wasm_eh + build_env['CXXFLAGS'] = build_env.get('CXXFLAGS', '') + _wasm_eh + build_env['LDFLAGS'] = build_env.get('LDFLAGS', '') + _wasm_eh + " -sWASM_BIGINT" IS_SINGLE_THREADED = True ENABLE_LTO = False # build with pthread doesn't work. The WASM bindings are also single threaded. @@ -305,6 +312,21 @@ class bdist_wheel(_bdist_wheel): def finalize_options(self): + if BUILD_PLATFORM == "emscripten": + # Under pyodide-build / `cibuildwheel --platform pyodide`, the + # authoritative wheel platform tag is handed to us verbatim via + # _PYTHON_HOST_PLATFORM. For PEP 783 (Pyodide >= 0.28 / "314") this + # is e.g. "pyemscripten_2026_0_wasm32" -- a tag PyPI accepts. The + # reconstruction below instead produced "emscripten__wasm32", + # which is locked to a Pyodide release and rejected by PyPI, so we + # defer to pyodide-build's tag when it is available. + host_platform = os.environ.get('_PYTHON_HOST_PLATFORM') + if host_platform: + self.plat_name = host_platform + else: + os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION else 'xxxxxx' + self.plat_name = f"emscripten_{os_version_tag}_wasm32" + return super().finalize_options() if BUILD_ARCH is not None and BUILD_PLATFORM is not None: os_version_tag = '_'.join(BUILD_OS_VERSION) if BUILD_OS_VERSION is not None else 'xxxxxx' os_version_tag = self.remove_build_machine_os_version(BUILD_PLATFORM, os_version_tag) From 99a8a922ec68e7d0ec8ab83d22823c3ce8ded3d2 Mon Sep 17 00:00:00 2001 From: davedets Date: Thu, 18 Jun 2026 12:36:14 -0700 Subject: [PATCH 2/3] Use "override" keyword where needed. (#9892) This is another PR towards the goal of getting Z3 to compile cleanly when included via FetchContents into clang-tidy, which uses a pretty strict set of warnings. The PR adds ``` "-Wsuggest-override" "-Winconsistent-missing-override" ``` to the CLANG_ONLY_WARNINGS. This exposes a relatively small number of places where method overrides did not use the "override" keyword. The PR fixes those. (In cmd_util.h, I also made the *_CMD macros be uniform in not ending the class they define with a semicolon; the invocation of the macro can add the semicolon.) --- cmake/compiler_warnings.cmake | 3 ++ src/cmd_context/cmd_util.h | 60 +++++++++++++---------- src/math/lp/dioph_eq.cpp | 2 +- src/math/lp/static_matrix.h | 22 +++++++-- src/solver/assertions/asserted_formulas.h | 10 ++-- 5 files changed, 59 insertions(+), 38 deletions(-) diff --git a/cmake/compiler_warnings.cmake b/cmake/compiler_warnings.cmake index d8dc90c05..38b442e83 100644 --- a/cmake/compiler_warnings.cmake +++ b/cmake/compiler_warnings.cmake @@ -20,6 +20,9 @@ set(CLANG_ONLY_WARNINGS "-Wno-c++98-compat" "-Wno-c++98-compat-pedantic" "-Wno-zero-length-array" + "-Wc99-extensions" + "-Wsuggest-override" + "-Winconsistent-missing-override" ) set(MSVC_WARNINGS "/W3") diff --git a/src/cmd_context/cmd_util.h b/src/cmd_context/cmd_util.h index 125ed6654..cec79a26f 100644 --- a/src/cmd_context/cmd_util.h +++ b/src/cmd_context/cmd_util.h @@ -17,45 +17,51 @@ Notes: --*/ #pragma once -#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \ +#define ATOMIC_CMD(CLS_NAME, NAME, DESCR, ACTION) \ +class CLS_NAME : public cmd { \ +public: \ + CLS_NAME():cmd(NAME) {} \ + char const * get_usage() const override { return 0; } \ + char const * get_descr(cmd_context & ctx) const override { \ + return DESCR; \ + } \ + unsigned get_arity() const override { return 0; } \ + void execute(cmd_context & ctx) override { ACTION } \ +} + +#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ class CLS_NAME : public cmd { \ public: \ CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return 0; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 0; } \ - virtual void execute(cmd_context & ctx) { ACTION } \ -}; - -#define UNARY_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ -class CLS_NAME : public cmd { \ -public: \ - CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return USAGE; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 1; } \ - virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { return ARG_KIND; } \ - virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \ + char const * get_usage() const override { return USAGE; } \ + char const * get_descr(cmd_context & ctx) const override { \ + return DESCR; \ + } \ + unsigned get_arity() const override { return 1; } \ + cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \ + return ARG_KIND; \ + } \ + void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \ } // Macro for creating commands where the first argument is a symbol // The second argument cannot be a symbol #define BINARY_SYM_CMD(CLS_NAME, NAME, USAGE, DESCR, ARG_KIND, ARG_TYPE, ACTION) \ class CLS_NAME : public cmd { \ - symbol m_sym; \ + symbol m_sym; \ public: \ CLS_NAME():cmd(NAME) {} \ - virtual char const * get_usage() const { return USAGE; } \ - virtual char const * get_descr(cmd_context & ctx) const { return DESCR; } \ - virtual unsigned get_arity() const { return 2; } \ - virtual void prepare(cmd_context & ctx) { m_sym = symbol::null; } \ - virtual cmd_arg_kind next_arg_kind(cmd_context & ctx) const { \ - return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \ + char const * get_usage() const override { return USAGE; } \ + char const * get_descr(cmd_context & ctx) const override { return DESCR; } \ + unsigned get_arity() const override { return 2; } \ + void prepare(cmd_context & ctx) override { m_sym = symbol::null; } \ + cmd_arg_kind next_arg_kind(cmd_context & ctx) const override { \ + return m_sym == symbol::null ? CPK_SYMBOL : ARG_KIND; \ } \ - virtual void set_next_arg(cmd_context & ctx, symbol const & s) { m_sym = s; } \ - virtual void set_next_arg(cmd_context & ctx, ARG_TYPE arg) { ACTION } \ -}; - + void set_next_arg(cmd_context & ctx, symbol const & s) override { m_sym = s; } \ + void set_next_arg(cmd_context & ctx, ARG_TYPE arg) override { ACTION } \ +} + class ast; class expr; diff --git a/src/math/lp/dioph_eq.cpp b/src/math/lp/dioph_eq.cpp index 0b3c5166a..c6719e0e3 100644 --- a/src/math/lp/dioph_eq.cpp +++ b/src/math/lp/dioph_eq.cpp @@ -580,7 +580,7 @@ namespace lp { const lar_term* m_t; undo_add_term(imp& s, const lar_term* t) : m_s(s), m_t(t) {} - void undo() { + void undo() override { m_s.undo_add_term_method(m_t); } }; diff --git a/src/math/lp/static_matrix.h b/src/math/lp/static_matrix.h index 08db2245d..c04728cc8 100644 --- a/src/math/lp/static_matrix.h +++ b/src/math/lp/static_matrix.h @@ -60,6 +60,14 @@ std::ostream& operator<<(std::ostream& out, const row_strip& r) { return out << "\n"; } +// Below, static_matrix has a superclass when Z3DEBUG is set, and some +// methods are overrides in that case. +#ifdef Z3DEBUG +#define DEBUG_OVERRIDE override +#else +#define DEBUG_OVERRIDE +#endif + // each assignment for this matrix should be issued only once!!! template class static_matrix @@ -119,9 +127,13 @@ public: void init_empty_matrix(unsigned m, unsigned n); - unsigned row_count() const { return static_cast(m_rows.size()); } + unsigned row_count() const DEBUG_OVERRIDE { + return static_cast(m_rows.size()); + } - unsigned column_count() const { return static_cast(m_columns.size()); } + unsigned column_count() const DEBUG_OVERRIDE { + return static_cast(m_columns.size()); + } unsigned lowest_row_in_column(unsigned col); @@ -197,7 +209,7 @@ public: void cross_out_row_from_column(unsigned col, unsigned k); - T get_elem(unsigned i, unsigned j) const; + T get_elem(unsigned i, unsigned j) const DEBUG_OVERRIDE; unsigned number_of_non_zeroes_in_column(unsigned j) const { return static_cast(m_columns[j].size()); } @@ -218,8 +230,8 @@ public: #ifdef Z3DEBUG unsigned get_number_of_rows() const { return row_count(); } unsigned get_number_of_columns() const { return column_count(); } - virtual void set_number_of_rows(unsigned /*m*/) { } - virtual void set_number_of_columns(unsigned /*n*/) { } + void set_number_of_rows(unsigned /*m*/) override { } + void set_number_of_columns(unsigned /*n*/) override { } #endif T get_balance() const; diff --git a/src/solver/assertions/asserted_formulas.h b/src/solver/assertions/asserted_formulas.h index 262499ff4..ba0b1f840 100644 --- a/src/solver/assertions/asserted_formulas.h +++ b/src/solver/assertions/asserted_formulas.h @@ -185,12 +185,12 @@ class asserted_formulas { public: \ FUNCTOR m_functor; \ NAME(asserted_formulas& af):simplify_fmls(af, MSG), m_functor ARG {} \ - virtual void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) { \ - m_functor(j.fml(), n, p); \ + void simplify(justified_expr const& j, expr_ref& n, proof_ref& p) override { \ + m_functor(j.fml(), n, p); \ } \ - virtual void post_op() { if (REDUCE) af.reduce_and_solve(); } \ - virtual bool should_apply() const { return APP; } \ - }; \ + void post_op() override { if (REDUCE) af.reduce_and_solve(); } \ + bool should_apply() const override { return APP; } \ + }; #define MK_SIMPLIFIERF(NAME, FUNCTOR, MSG, APP, REDUCE) MK_SIMPLIFIERA(NAME, FUNCTOR, MSG, APP, (af.m), REDUCE) From 225ac56f5a39fe46fa12757a67f02aa6075dbec8 Mon Sep 17 00:00:00 2001 From: Nikolaj Bjorner Date: Thu, 18 Jun 2026 12:39:39 -0700 Subject: [PATCH 3/3] extend cases for process_formulas_on_stack Signed-off-by: Nikolaj Bjorner --- src/smt/smt_model_finder.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/smt/smt_model_finder.cpp b/src/smt/smt_model_finder.cpp index 27516b3dc..bfecc51ac 100644 --- a/src/smt/smt_model_finder.cpp +++ b/src/smt/smt_model_finder.cpp @@ -2203,9 +2203,12 @@ namespace smt { if (is_app(curr)) { if (to_app(curr)->get_family_id() == m.get_basic_family_id() && m.is_bool(curr)) { switch (static_cast(to_app(curr)->get_decl_kind())) { - case OP_IMPLIES: + case OP_IMPLIES: + process_literal(to_app(curr)->get_arg(0), neg(pol)); + process_literal(to_app(curr)->get_arg(1), pol); + break; case OP_XOR: - UNREACHABLE(); // simplifier eliminated ANDs, IMPLIEs, and XORs + process_iff(to_app(curr)); break; case OP_OR: case OP_AND: