feat!: full mp_real_<P> / mp_complex_<P> numeric API; Number rewritten on top#37
Merged
Conversation
…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>
This was referenced Jun 2, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Overhauls the C++/Python boundary around the arbitrary-precision number types. The original scope ("expose more
mp_realprecisions") expanded into a full bidirectional API and a rewrite of the PythonNumberclass on top of it.mp_real_<P>— was a singlemp_real_24with__init__+.str(). Now every precision binds the full set of arithmetic and comparison operators,__pow__,__abs__,__hash__,__repr__, and.str().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.p_8192top_262144.AllowedPrecisionsSeq(astd::integer_sequence<unsigned, …>) drives the eval variants, the init dispatch, and the Python bindings — adding a precision is now one line..so~25%.strip_neg_zero.hpp): all.str()/.real()/.imag()output renders+0instead of-0. Fixes the family of byte-pair equality artifacts that PR feat(Number)!: simpler API, reverse arithmetic, complex equality #18 had toxfail(e.g.i*i*i*i).Formula.evaluate()returns a typed mp object. Not a string — the returned object's class (mp_real_<P>vsmp_complex_<P>) carries the real/complex kind.NumberPython class rewritten. Eager evaluation viaevaluate(), 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 raisesTypeErrorinstead of returning a fake answer.formula.backendmodule bridging Python to the per-precision class lookup. PublicMAX_PRECISIONis derived from what the C++ side actually exposes.Closes the original
// TODO support all mp_realatsrc/cpp/main.cpp:145and the implicit "expose mp_complex too" follow-up.Changes
C++ side
src/cpp/csconstants.hpp—AllowedPrecisionsenum reorganized: 16, 24, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144 (16 values, was 18). NewAllowedPrecisionsSeq = std::integer_sequence<unsigned, p_16, …, p_262144>is the single source of truth. Helpersseq_to_arrayandseq_to_tuplederiveprecisions_array/precisionsautomatically.kPrecisionsLengthbecomesAllowedPrecisionsSeq::size().src/cpp/csformula/csformula.hpp—CSEvalVariant/CSEvalComplexVariantderived fromAllowedPrecisionsSeqvia amake_eval_variant<Seq>template (replaces ~38 hardcodedstd::shared_ptr<cseval<…>>,lines). NewFormula::visit_value<Visitor>template applies a visitor to the typed eval node, used byevaluate().src/cpp/bindings_mp_real.cpp(new) —register_mp_real(py::module_&)emits onepy::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)withstrip_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__returnsmp_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 == -0at the value level, and exposing the sign was breaking byte-string equality.src/cpp/main.cpp— slimmed: declaresregister_mp_real/register_mp_complex, adds aGetValueVisitorthat casts the evaluated value to a Pythonmp_real_<P>/mp_complex_<P>object, adds theFormula.evaluate()pybind binding, calls the two register functions inPYBIND11_MODULE.Python side
src/formula/backend.py(new) — single Python place that encodes themp_real_<P>/mp_complex_<P>naming. Exportsmp_class(precision, is_complex),REAL_TYPES,COMPLEX_TYPES,MAX_PRECISION. The constant is computed at import time from what_formulaactually exposes.src/formula/__init__.py— re-exports the new backend names alongside the existingFormula/Solver/Number/FmtFlags.src/formula/formula.py—Numberrewritten end-to-end.__init__eagerly evaluates viaSolver(...).evaluate()and wraps the typed mp object asself._value. New_wrapclassmethod for internal construction without re-eval. Arithmetic uses Python'soperatormodule againstself._valuedirectly._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 throughNumber(...):"3+4*i","-1-2*i","5*i", plain"-1"when imag collapses, plain"0"for zero.__repr__is the eval-able debug formNumber('3+4*i', precision=24). Complex ordering raisesTypeError("complex numbers are not orderable")._cmpreturnsNotImplementedfor 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. Newextra_link_argsstrips symbol tables (-Wl,-son Linux,-Wl,-xon 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 exactlyAllowedPrecisions" 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 (assertsTypeErroron<), 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^4keeps its tiny imag, doesn't lie),str(a) == str(b)whenevera == b,Number(str(n)) == nround-trip across real/complex/negative-imag/pure-imag/high-precision/scientific/drift,eval(repr(n)) == n.tests/test_number_{complex,constructor_types,eq_notimplemented,hashable,reverse_arithmetic,comparison_precision}.pyfor the new internals.tests/test_number_fixed.pydeleted (78 lines) — the.fixedproperty is gone.Breaking changes
Number.fixedandNumber.pair_fixedare removed. Usestr(n)for the value form, or readn._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._expressionis gone. The internal state is nowself._value(a typedmp_real_<P>/mp_complex_<P>) plusself._precision. Code readingn._expressionwon't compile.Numberarithmetic now returns a Number wrapping a typed C++ mp value, not a Number wrapping a symbolic / formatted string. Callers that round-tripped throughn.expressionneed to switch tostr(n)or read._valuedirectly.TypeErrorwith the message"complex numbers are not orderable", instead of returning whatever the old string-comparison path produced.Solver/Formulawithprecision ∈ {48, 96, 192, 384, 768, 3072, 6144}no longer matches anAllowedPrecisionsvalue exactly. The existingprepare_precisionlogic 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 meansisinstance(x, mp_real_24)is unchanged but downstream code that expectedmp_real_24to have only__init__+.str()may see new attributes. The previously-bound surface is a strict subset of the new one.