Skip to content

feat!: full mp_real_<P> / mp_complex_<P> numeric API; Number rewritten on top#37

Merged
hozblok merged 5 commits into
masterfrom
feat/mp-real-all-precisions
Jun 2, 2026
Merged

feat!: full mp_real_<P> / mp_complex_<P> numeric API; Number rewritten on top#37
hozblok merged 5 commits into
masterfrom
feat/mp-real-all-precisions

Conversation

@hozblok
Copy link
Copy Markdown
Owner

@hozblok hozblok commented May 19, 2026

Summary

Overhauls the C++/Python boundary around the arbitrary-precision number types. The original scope ("expose more mp_real precisions") expanded into a full bidirectional API and a rewrite of the Python Number class on top of it.

  • Full numeric API on mp_real_<P> — was a single mp_real_24 with __init__ + .str(). Now every precision binds the full set of arithmetic and comparison operators, __pow__, __abs__, __hash__, __repr__, and .str().
  • New mp_complex_<P> bindings with the same shape (minus ordering, since complex isn't ordered) plus a 2-arg (real, imag) constructor and .real() / .imag() accessors.
  • Precision ladder reshaped. Dropped non-power-of-two intermediates (48, 96, 192, 384, 768, 3072, 6144). Extended the top end far past p_8192 to p_262144.
  • Single source of truth. New AllowedPrecisionsSeq (a std::integer_sequence<unsigned, …>) drives the eval variants, the init dispatch, and the Python bindings — adding a precision is now one line.
  • Build split across three translation units to bound per-compile memory; optional link-time symbol stripping shrinks the .so ~25%.
  • Signed-zero normalization in C++ (strip_neg_zero.hpp): all .str() / .real() / .imag() output renders +0 instead of -0. Fixes the family of byte-pair equality artifacts that PR feat(Number)!: simpler API, reverse arithmetic, complex equality #18 had to xfail (e.g. i*i*i*i).
  • Formula.evaluate() returns a typed mp object. Not a string — the returned object's class (mp_real_<P> vs mp_complex_<P>) carries the real/complex kind.
  • Number Python class rewritten. Eager evaluation via evaluate(), wraps a typed C++ mp value, arithmetic runs directly on it (no string round-trip). New math-style __str__ and eval-able __repr__. Complex ordering raises TypeError instead of returning a fake answer.
  • New formula.backend module bridging Python to the per-precision class lookup. Public MAX_PRECISION is derived from what the C++ side actually exposes.

Closes the original // TODO support all mp_real at src/cpp/main.cpp:145 and the implicit "expose mp_complex too" follow-up.

Changes

C++ side

  • src/cpp/csconstants.hppAllowedPrecisions enum reorganized: 16, 24, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144 (16 values, was 18). New AllowedPrecisionsSeq = std::integer_sequence<unsigned, p_16, …, p_262144> is the single source of truth. Helpers seq_to_array and seq_to_tuple derive precisions_array / precisions automatically. kPrecisionsLength becomes AllowedPrecisionsSeq::size().
  • src/cpp/csformula/csformula.hppCSEvalVariant / CSEvalComplexVariant derived from AllowedPrecisionsSeq via a make_eval_variant<Seq> template (replaces ~38 hardcoded std::shared_ptr<cseval<…>>, lines). New Formula::visit_value<Visitor> template applies a visitor to the typed eval node, used by evaluate().
  • src/cpp/bindings_mp_real.cpp (new) — register_mp_real(py::module_&) emits one py::class_<mp_real<P>> per precision via fold expression. Per-class surface: __init__(str), + - * / (binary), unary -, == != < <= > >=, __pow__, __abs__, __hash__ (string-based), __repr__ (mp_real_24('3.14')), .str(digits, format) with strip_neg_zero.
  • src/cpp/bindings_mp_complex.cpp (new) — register_mp_complex(py::module_&). Same shape minus ordering operators; plus a 2-arg __init__(real, imag), .real(digits, format), .imag(digits, format). __abs__ returns mp_complex_<P> (real magnitude wrapped back as complex). .str() collapses zero-imag to plain real.
  • src/cpp/strip_neg_zero.hpp (new) — helper that turns "-0" / "-0.0" / "-0.000…" into the unsigned form. Applied at every string-producing binding. Comment notes that MPFR keeps a sign bit on zero per IEEE-754, but +0 == -0 at the value level, and exposing the sign was breaking byte-string equality.
  • src/cpp/main.cpp — slimmed: declares register_mp_real / register_mp_complex, adds a GetValueVisitor that casts the evaluated value to a Python mp_real_<P> / mp_complex_<P> object, adds the Formula.evaluate() pybind binding, calls the two register functions in PYBIND11_MODULE.

Python side

  • src/formula/backend.py (new) — single Python place that encodes the mp_real_<P> / mp_complex_<P> naming. Exports mp_class(precision, is_complex), REAL_TYPES, COMPLEX_TYPES, MAX_PRECISION. The constant is computed at import time from what _formula actually exposes.
  • src/formula/__init__.py — re-exports the new backend names alongside the existing Formula/Solver/Number/FmtFlags.
  • src/formula/formula.pyNumber rewritten end-to-end. __init__ eagerly evaluates via Solver(...).evaluate() and wraps the typed mp object as self._value. New _wrap classmethod for internal construction without re-eval. Arithmetic uses Python's operator module against self._value directly. _align(other) promotes both sides to a common precision and kind (higher precision and complex win). _pair() returns the canonical (real_str, imag_str) tuple used by __eq__ / __hash__. __str__ is math-style and round-trips through Number(...): "3+4*i", "-1-2*i", "5*i", plain "-1" when imag collapses, plain "0" for zero. __repr__ is the eval-able debug form Number('3+4*i', precision=24). Complex ordering raises TypeError("complex numbers are not orderable"). _cmp returns NotImplemented for foreign types.

Build

  • setup.py — bindings now compile across three translation units (main.cpp, bindings_mp_real.cpp, bindings_mp_complex.cpp) to bound per-compile memory. New extra_link_args strips symbol tables (-Wl,-s on Linux, -Wl,-x on macOS; Windows already isolates debug info to a separate .pdb).

Docs

  • CLAUDE.md — minor tightening.
  • doc/benchmarks.md, doc/benchmark-{dec-float-scaling,end-to-end-sympy,raw-arithmetic,transcendentals}.md, doc/precision-ladder.md — new documentation around the precision set and performance characterization.

Test plan

  • tests/test_mp_real_all_precisions.py (89 lines, rewritten): parametrized over the 16 precisions — class existence, "bound set is exactly AllowedPrecisions" guard, str round-trip, arithmetic, comparison-by-value, hash agrees with eq, repr round-trips, higher precision retains more digits, unknown precision absent.
  • tests/test_mp_complex_all_precisions.py (110 lines, new): mirror for complex — class existence, bound-set guard, str round-trip, zero-imag renders as real, pair constructor, real() / imag() accessors, arithmetic (incl. i*i == -1, (1+i)*(1-i) == 2, |3+4i| == 5), no ordering operators (asserts TypeError on <), equality by value, hash agreement, repr round-trips.
  • tests/test_number_str_repr.py (268 lines, new): exhaustive __str__ / __repr__ contract — value form, zero-imag collapse, sign placement, scientific-notation cases, drift preservation (i^4 keeps its tiny imag, doesn't lie), str(a) == str(b) whenever a == b, Number(str(n)) == n round-trip across real/complex/negative-imag/pure-imag/high-precision/scientific/drift, eval(repr(n)) == n.
  • Updates to tests/test_number_{complex,constructor_types,eq_notimplemented,hashable,reverse_arithmetic,comparison_precision}.py for the new internals.
  • tests/test_number_fixed.py deleted (78 lines) — the .fixed property is gone.

Breaking changes

  • Number.fixed and Number.pair_fixed are removed. Use str(n) for the value form, or read n._pair() (the canonical (real_str, imag_str) tuple) for the internal raw form. The wrapped-mp-value design makes the old eager-format properties redundant.
  • Number._expression is gone. The internal state is now self._value (a typed mp_real_<P> / mp_complex_<P>) plus self._precision. Code reading n._expression won't compile.
  • Number arithmetic now returns a Number wrapping a typed C++ mp value, not a Number wrapping a symbolic / formatted string. Callers that round-tripped through n.expression need to switch to str(n) or read ._value directly.
  • Complex ordering raises TypeError with the message "complex numbers are not orderable", instead of returning whatever the old string-comparison path produced.
  • Precision ladder changed. Constructing a Solver / Formula with precision ∈ {48, 96, 192, 384, 768, 3072, 6144} no longer matches an AllowedPrecisions value exactly. The existing prepare_precision logic rounds up to the next supported precision, so the call still works but the effective precision is higher than requested. Top end gained 16384 → 262144.
  • mp_real_<P> constructors now accept the full numeric API, which means isinstance(x, mp_real_24) is unchanged but downstream code that expected mp_real_24 to have only __init__ + .str() may see new attributes. The previously-bound surface is a strict subset of the new one.

hozblok and others added 5 commits May 20, 2026 00:41
…DO main.cpp:145)

Only mp_real_24 was bound. Generated one py::class_<mp_real<P>> per
AllowedPrecisions value via a small helper + std::integer_sequence fold,
yielding mp_real_16, mp_real_24, ..., mp_real_8192 (18 classes total).

Same surface as the previous mp_real_24: __init__(str) + str(digits, f).

New tests in tests/test_mp_real_all_precisions.py parametrize over the
18 precisions: class existence, "3.14".str(10) round-trip, default-arg
str() call. Full suite 442/442 (was 388, +54 new), 3 xfailed unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add arithmetic, comparison, pow, abs, hash and repr to mp_real_<P>.
Derive the precision list from a single AllowedPrecisionsSeq.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread tests/test_mp_complex_all_precisions.py Dismissed
Comment thread tests/test_number_str_repr.py Dismissed
@hozblok hozblok changed the title feat: expose mp_real<P> bindings for all AllowedPrecisions feat!: full mp_real_<P> / mp_complex_<P> numeric API; Number rewritten on top Jun 2, 2026
@hozblok hozblok marked this pull request as ready for review June 2, 2026 14:07
@hozblok hozblok merged commit 064c1d3 into master Jun 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants