Uni20
Asynchronous tensor and runtime library.
Loading...
Searching...
No Matches
Scalar Type Policy

Optional arbitrary-precision real scalars use uni20::mpreal with explicit runtime Precision when UNI20_ENABLE_MPREAL=ON. The same scalar-family option supplies MPC-backed complex<mpreal> and make_complex_t<mpreal>; there is no separate complex-scalar switch. Default-constructed MPFR/MPC scalars are exact zero; explicit uninitialized supplies an unset placeholder. They hold exact rationals until finite working precision is supplied. Their precision cannot be described by type-only numerical limits. Their Real/Complex traits do not imply BLAS/LAPACK support. Tensor allocation and dense backend integration are separate from scalar support. UNI20_BACKEND_MPLAPACK=ON adds a matrix-product, solve and dense-decomposition provider, selected through BlasBackend and LapackBackend, without extending the type-only LapackScalar concepts or generic Krylov support.

Prefer mpreal for general numerical code: it supports both exact rationals and finite-precision approximations. Use uni20::exact_rational when the type must explicitly require rational arithmetic, for example when generating exact coefficients. It is the owning GMP rational payload used by mpreal and by precisionless literal arithmetic; it has no approximation precision of its own.

uni20::complex<T> selects the appropriate owning scalar type. Existing native real types retain exact std::complex<T> type identity. Generic complex APIs should deduce the complex type directly (template <Complex C>) and obtain its real type through make_real_t<C>; deduction through the selecting alias is not supported, including in partial specializations or nested container parameters. Tests should exercise deduction without explicit template arguments or optional real-valued arguments that could mask this limitation. Internal adaptations specifically for the standard-library family use detail::standard_complex<T>, including its fixed-precision LAPACK ABI and CUDA storage/execution adapters. MPC component access returns owning real values; setters convert to the existing complex precision. Do not reinterpret an MPC value as adjacent C++ real objects.

Runtime-precision host tensors take a trailing Precision and carry a construction default independently of their elements. Parent-backed structural views inherit that default, resolving it within the readable parent epoch for async aliases; changing a default never converts stored values. Use at_precision(tensor, p) for explicit bulk conversion. Fixed-precision storage and views have no corresponding runtime state. See tensor construction defaults for output allocation and view metadata semantics.

This page records the project scalar spelling and concept policy. The concrete aliases live in src/uni20/core/types.hpp; scalar traits and concepts live in src/uni20/core/scalar_traits.hpp and src/uni20/core/scalar_concepts.hpp.

Generic precision

core/precision.hpp defines Precision in every build, without an MPFR dependency. It describes exact arithmetic or a positive binary significand size; decimal_digits(n) requests enough bits for at least n decimal digits. Providers check their own representable precision ranges before allocation or numerical work. There is no ambient or native precision value.

core/runtime_precision.hpp supplies the scalar queries and construction helper:

auto zero = uni20::scalar_at_precision<Real>(0, p);
locale_t value
Definition data_table.cpp:49
Definition data_table.cpp:8
constexpr Precision precision_of(T const &value)
Read scalar precision without changing any scalar or provider state.
Definition runtime_precision.hpp:53
Project-level numeric limits customization point.
Definition numeric_limits.hpp:16

For native real or complex values, precision_of returns the significand size of the real component type. native_precision<T>() is its constexpr, type-only counterpart. Exact scalar types such as integers and half-integers report Precision::exact(). Runtime scalars report their actual exact or finite state; unset values throw. scalar_at_precision<T>(value, p) constructs runtime values at p; for fixed types it requires exactly their native precision. It never silently lowers an incompatible precision request.

tensor/precision.hpp extends precision_of to tensors and views. It reads the construction default of runtime tensors, without inspecting elements; fixed element types report native precision without storing metadata. A runtime tensor without a default cannot supply an inferred precision. Parent-backed views keep following their parent's default. Host tensor constructors and at_precision(tensor, p) accept precision for fixed types too, validating the match before allocation or copying.

The matrix-product, GEMM, square-solve, LU, log-determinant, QR/LQ, self-adjoint and general eigensystem, SVD and truncated-SVD interfaces accept the same explicit precision argument for native and runtime scalars. Omitting it selects precision from participating tensor defaults; exact defaults are neutral, and different finite defaults require an override. An explicit request fixes the result precision: native results must match their type, while runtime conversion happens inside the backend. Approximation-only operations require finite precision. Async operations infer defaults after awaiting their readable inputs.

LU refinement explicitly separates factor, working and residual scalar/precision roles. Each fixed role still requires its native precision. The solver owns the conversions between its distinct workspaces; individual LU kernels retain their existing factor-precision contract.

This does not add a precision argument to every numerical operation or expand provider coverage. Fixed kernels retain type-defined precision; runtime kernels receive the resolved precision through dispatch. The MPLAPACK integration documents current coverage and provider-specific state. The runnable generic precision example solves the same system with double and, when configured, MPFR at 128 and 256 bits.

Scalar Spelling

Uni20 code should spell real and complex scalar types through the project-level aliases when an alias exists.

The canonical real aliases are:

alias meaning availability
uni20::float32 float always
uni20::float64 double always
uni20::float80 native x87 extended precision, alias of long double only when UNI20_HAS_FLOAT80=1
uni20::float128 configured binary128 real scalar only when UNI20_HAS_FLOAT128=1

uni20::float80 has 64 significand bits (about 19 decimal digits) and the x87 extended exponent range. CMake automatically detects the native long double format from its numerical limits; it does not infer precision from sizeof. An fp80 object can occupy 12 or 16 bytes because of padding. complex160 aliases complex<float80> under the same availability guard. There is no software emulation or silent substitution: on platforms where long double is binary64 or binary128, UNI20_HAS_FLOAT80=0 and the alias is absent. Ordinary long double remains a valid Uni20 real scalar on all those platforms. Guard direct references to uni20::float80 with UNI20_HAS_FLOAT80.

uni20::float128 is a configuration-dependent type. In the current MPLAPACK configuration it aliases mplapack_binary128_t, whose concrete spelling is selected by the resolved MPLAPACK package. Code that is not gated by UNI20_HAS_FLOAT128 must not name uni20::float128.

When MPFR is also enabled, binary128 imports use mpreal{value, precision} and exports use static_cast<float128>(value). Both avoid intermediate narrowing to long double. Approximate and exact-rational exports round to nearest, ties to even, directly in the destination format, including subnormals. See MPFR conversions for the dependency and rounding contracts.

Runtime-facing code should use uni20::ScalarPrecision and uni20::visit_scalar_precision from uni20/core/scalar_precision.hpp instead of repeating configuration guards. The precision enum always recognizes fp32, fp64, fp80, and fp128; the visitor throws when a recognized precision is not configured. The visitor is the central preprocessor boundary that maps a runtime precision to uni20::float32, uni20::float64, or the conditional uni20::float80 and uni20::float128 types. uni20::configured_scalar_precisions() and uni20::configured_scalar_precision_choices() expose the available set for help text, diagnostics, examples, and future language bindings.

To enable fp128 scalars, configure with UNI20_ENABLE_FLOAT128=ON; add UNI20_BACKEND_MPLAPACK=ON for the linear-algebra kernels. Scalar arithmetic currently uses MPLAPACK's binary128 support even with kernels disabled. CMake prefers a compatible installed MPLAPACK 3.0.0 or newer package and otherwise fetches the pinned 3.0.0 release. See scalar configuration for the independent family/backend choices, and MPLAPACKBinary128 Setup" for dependency selection, optional system-package, and validation commands. For native real types, <tt>uni20::complex\<T\></tt> retains standard-library ABI, layout and interoperability. The MPC specialization has its own representation and must be explicitly converted at provider boundaries. Owning Tensor shape construction initializes stored numerical elements to zero. <tt>uni20::uninitialized</tt> explicitly requests storage whose values must be supplied before use. Copies and materializations initialize from their source, and internal overwrite outputs request uninitialized allocation to avoid an extra zero-fill. The low-level <tt>HostBuffer(size)</tt> retains its allocation-only numerical contract; initialization-aware storage factories receive a <tt>StorageInitialization</tt> choice from the tensor layer. The public <tt>uni20::enable_uninitialized_storage\<T\></tt> variable template in <tt>common/initialization.hpp</tt> governs allocation and lifetime mechanics, not the Tensor default. Its conservative default accepts trivially copyable and trivially destructible types. Uni20 explicitly opts <tt>uni20::complex\<Real\></tt> into this behavior because the project relies on its scalar-array representation and trivial lifetime behavior on supported standard libraries. Extension value types may specialize this trait only when allocation, copying object representations, and release without constructors/destructors form a valid lifetime model. Other types retain required object construction and destruction. Zero construction requires a numerical zero or a valid <tt>T{}</tt> value; requesting it for a type without either value is rejected. Raw uninitialized allocation remains available for lifetime-safe types without a default constructor. Optional diagnostic filling uses <tt>uni20::numeric_limits\<Real\>\::signaling_NaN()</tt> when <tt>has_signaling_NaN</tt> is true, including both components of complex values. Unsupported scalar types are not given an invented sentinel. MemorySanitizer and Memcheck annotations can still track lifetime-safe uninitialized storage without a NaN representation. Diagnostic filling and initialization tracking are independent of <tt>NDEBUG</tt>; see <a href="../development/testing.md::initialization-diagnostics" >testing</a>. The canonical complex aliases are: <table class="markdownTable"> <tr class="markdownTableHead"> <th class="markdownTableHeadNone"> alias

meaning

uni20::complex<T>

project-level complex scalar alias

uni20::complex64, uni20::cfloat

uni20::complex<float>

uni20::complex128, uni20::cdouble

uni20::complex<double>

uni20::complex256, uni20::cfloat128

uni20::complex<uni20::float128> when UNI20_HAS_FLOAT128=1

Project code, tests, examples, and documentation should use uni20::complex<T> unless they are explicitly documenting or testing the alias relationship to std::complex<T>, or they are at a narrow external interop boundary that must name the standard-library type.

CUDA buffers and pointer accessors require trivially copyable element types. This excludes owning mpreal and complex<mpreal> objects from device storage and bytewise host/device transfers, including uninitialized allocation. Convert them explicitly to a supported native scalar before transfer. Trivial copyability is a representation requirement, not a claim that device arithmetic or a provider kernel exists for every admitted type.

CUDA execution does not change the logical or persistent scalar type. CudaTensor<uni20::complex<T>> stores the same uni20::complex<T> objects as the corresponding host tensor. CUDA accessors load and store their guaranteed two real components through cuda::std::complex<T> execution values and assignable proxy references. cuComplex and cuDoubleComplex remain localized provider ABI types at cuBLAS call boundaries. uni20::logical_value_t<T> maps a domain-specific execution value back to the backend-neutral logical scalar advertised by expression accessors.

Scalar Concepts

Scalar-generic code should prefer the scalar traits in uni20/core:

trait/concept use
uni20::Real<T> real scalar constraints
uni20::Complex<T> complex scalar constraints
uni20::RealOrComplex<T> real-or-complex scalar constraints
uni20::ScalarValued<T> scalar, container, or view whose recursive value_type resolves to a scalar
uni20::RealScalarValued<T> scalar-valued type whose extracted scalar is real
uni20::ComplexScalarValued<T> scalar-valued type whose extracted scalar is complex
uni20::IntegerScalarValued<T> scalar-valued type whose extracted scalar is integer
uni20::RealOrComplexScalarValued<T> scalar-valued type whose extracted scalar is real or complex
uni20::BlasReal<T> real scalar with a configured dense BLAS-style backend
uni20::BlasComplex<T> complex scalar with a configured dense BLAS-style backend
uni20::LapackReal<T> real scalar with a configured dense LAPACK-style backend
uni20::LapackComplex<T> complex scalar with a configured dense LAPACK-style backend
uni20::LapackRealOrComplex<T> real or complex scalar whose underlying real precision has dense real LAPACK coverage
uni20::LapackComplexReal<T> real precision whose uni20::complex<T> has dense complex LAPACK coverage
uni20::make_real_t<T> underlying real scalar
uni20::make_complex_t<T> complexified scalar/container type
uni20::scalar_t<T> scalar extracted from a container-like type
uni20::numeric_limits<T> project-level numeric limits customization point
uni20::isfinite(x) project-level finite-value predicate for integer, real, and complex scalars

Scalar, Real, Complex, Integer, and RealOrComplex constrain the type itself. Use them for scalar-only helpers such as uni20::conj, uni20::herm, or scalar arithmetic kernels. The ScalarValued family follows scalar_t<T> through recursive value_type definitions, so it may match containers, views, mdspans, or future tensor types. Use scalar-valued concepts when an algorithm is generic over an object that carries scalar elements, not when the parameter must itself be the scalar value.

Blas* and Lapack* describe configured backend support. This distinction matters for extension scalar types: a type can be a valid Uni20 real scalar without having BLAS or LAPACK coverage in the current build.

The broad traits currently describe the fixed-scalar raw-wrapper surface, not every scalar accepted by every operation in a backend family. In particular, configured MPFR and binary80 products, LU and decompositions use BlasBackend/LapackBackend while remaining outside these traits. Use probe_dispatch_kernel_types for a particular operation and operand signature. The integration roadmap tracks the remaining projected-algorithm capability audit.

In particular, float80 and complex<float80> reuse the native long double scalar and generic CPU paths where supported. The optional MPLAPACK binary80 adapter adds GEMM, square solves, reusable LU, QR/LQ, self-adjoint/general eigensystems and SVD through BlasBackend and LapackBackend. Complex provider LU and decompositions require its scalar to be long double; distinct _Float64x complex division is unsafe, so default complex solves use the CPU path instead. Complex decompositions have no CPU fallback. The adapter keeps provider types private and converts by value. This limited coverage does not make these types satisfy the broad Blas*/Lapack* traits or add GPU support. Runtime precision visitors instantiate every configured scalar: clients requiring LAPACK must guard those instantiations with the relevant capability concept and reject unsupported requests, rather than silently narrowing. Native fp80 availability does not extend the supported precisions of the projected LAPACK-based Krylov solvers or DMRG.

Reference sums and inner products use compensated accumulation in the input scalar field. Norms use scaled sum-of-squares arithmetic in the associated real field. Algorithms must choose their numerical accumulation method explicitly; Uni20 does not define a universal "next wider" scalar because no such type exists for the widest configured precision, and silent promotion would make result and performance behavior depend on the input dtype. Integer sums are not accepted until Uni20 defines their overflow contract.

The CPU dense matrix exponential likewise performs norm estimation and scaling-exponent arithmetic in the matrix scalar's real field. Its overflow protection comes from logarithmic entry bounds and prescaling before high-order matrix powers, so binary128 support does not depend on a nonexistent wider floating-point type.

For matrix-free algorithms, avoid duplicating scalar type information in interfaces when it can be inferred from the vector operations. For example, the return type of inner_product(x, y) names the vector scalar field, and norm(x) returns the associated real scalar. Additional scalar declarations should only be added when they encode information that cannot be inferred from the operation interface.

Future higher-precision real types should be integrated by extending the Uni20 real scalar traits, uni20::numeric_limits<T>, and the required linear algebra backends. uni20::BlasReal<T>, uni20::BlasComplex<T>, uni20::LapackReal<T>, and uni20::LapackComplex<T> are intentionally backend-relative and separately extensible: they mean Uni20 has configured dense BLAS-style or LAPACK-style implementations for that scalar, not that the type is one of the standard Fortran BLAS/LAPACK ABI scalar types. A type may eventually satisfy only one of these concepts if Uni20 has only one backend layer for it. For example, an MPLAPACK-enabled build may make mplapack_binary128_t satisfy both BlasReal and LapackReal, while uni20::complex<mplapack_binary128_t> satisfies the corresponding complex concepts only for paths with explicit complex MPBLAS/MPLAPACK wrappers. The BLAS side can be project overloads backed by MPBLAS entry points rather than s/d/c/z Fortran ABI symbols.

Krylov algorithms that form dense projected Hermitian problems use LapackRealOrComplex: a complex vector field can still reduce to a real projected tridiagonal problem. Algorithms that form dense complex projected problems use LapackComplexReal or LapackComplex, because real LAPACK support for the underlying precision is not sufficient there.

uni20::make_complex_t<T> follows the Uni20 real scalar traits rather than std::floating_point<T>, so extension real types can opt into the scalar model without depending on standard-library concept recognition. If the platform supports std::complex<Real> for such a real type, uni20::complex<Real> should continue to be the preferred spelling. If a future backend requires a different complex representation, that change should happen behind the project-level alias/traits boundary rather than by scattering backend-specific complex types through algorithms.

Scalar Math

Use the callable operations in uni20::math from <uni20/core/math.hpp> for scalar-generic host math, such as math::sqrt, math::abs and math::exp. They dispatch to native or class-specific ADL overloads without a fallback conversion of unfamiliar classes to double. Native real results cannot narrow their operands' precision. math::real and math::imag read components by value; real and integral scalars retain their type, and imaginary zero retains runtime precision. Scalar exactness and explicit Precision rules are unchanged. See shared scalar math dispatch for the operation list, extension contract and host-execution boundary. Do not duplicate subsystem-private adl_* helpers for these operations.

Scalar-generic code should use uni20::isfinite(x) instead of directly calling std::isfinite(x) when x may be a Uni20 scalar. Integers are always finite, real scalars are checked for NaN and positive/negative infinity through uni20::numeric_limits<T>, and complex scalars are finite only when both components are finite. This keeps extension scalar support behind the same project customization points as scalar spelling and numeric limits.

Use uni20::pi<Real>, uni20::log_two<Real> and uni20::euler_gamma<Real> from <uni20/core/math_constants.hpp> (also included by math.hpp) for the shared native/MPFR constant names. Native specializations are constexpr values of type Real, backed by std::numbers; configured binary128 retains its full precision. With mpreal.hpp included, the MPFR specializations are precision-aware descriptors: use .at(p) or arithmetic with an approximate operand to select finite precision. catalan<Real> is also available for native formats and mpreal, with the same type/precision conventions. Native Catalan is assembled from a binary prefix and independently checked for correct rounding. See native constants.

Other native mathematical constants can use typed std::numbers values. Do not widen an untyped double constant or a decimal double literal into a higher-precision scalar. Do not choose ambient precision for an MPFR constant.

Use uni20::factorial<Real>(n) when the result type cannot be deduced from its integer argument. The form uni20::factorial<Real>(n,p) supports generic precision-aware code: fixed types validate p against their native precision; mpreal requires explicit finite or exact precision. See typed factorials.

Floating-Point Comparisons

uni20/common/floating_eq.hpp provides ULP comparisons for binary32, binary64, configured binary128, and native fp80, including their uni20::complex<T> counterparts. IeeeBinaryReal still denotes only the supported IEEE interchange layouts; UlpOrderedReal also admits native fp80. The fp80 ordering uses exact exponent/significand decomposition, not object bytes, so storage padding and byte order do not affect the distance.

uni20/common/gtest.hpp exposes this through EXPECT_FLOATING_EQ and ASSERT_FLOATING_EQ. The optional third argument is a nonnegative ULP bound (default four). Signed zeros compare equal; NaNs never do; infinities match only when equal. The comparison uses the full distance even when the signed diagnostic distance saturates at std::numeric_limits<long long>::max(). The tracing CHECK_FLOATING_EQ and PRECONDITION_FLOATING_EQ assertions share the same comparison implementation.

With MPFR enabled, FloatingULP<mpreal> counts steps on the shared runtime precision and current MPFR exponent range. Approximate operands must have matching precision. One exact operand is rounded to the approximate operand's precision; two exact operands require exact equality. Unset operands and precision mismatches fail with explicit GoogleTest diagnostics. No native conversion or ambient precision is used, and comparison preserves MPFR flags. UlpOrderedReal remains the native ordering concept; UlpComparable also admits mpreal. complex<mpreal> ULP comparison is deferred. See the assertion contracts for rounding, endpoint and diagnostic details.

Use ULP bounds for contracts expressed in representable-value steps. Algorithmic residuals, truncation errors, and reference data with limited accuracy generally need explicit absolute/relative tolerances in the native scalar type instead. Do not narrow fp80, fp128, or MPFR through GoogleTest's double-based EXPECT_NEAR.

Scalar Formatting

Scalar-generic diagnostics and presentation code should use uni20::format_scalar or the corresponding presentation helpers rather than assuming standard stream or formatter support. Trace formatting recognizes all types satisfying Uni20's Real and Complex concepts, including uni20::float128 and uni20::complex<uni20::float128> when configured.

Native float80 uses the existing long double parsing, formatting, and presentation precision policy; no conversion through double is involved.

presentation::DataTableValue admits supported fixed-precision real types and, with MPFR enabled, mpreal and optional mpreal. Tables and metadata_value retain per-value precision or exact rational state and reject unset values. Conversions from text or native numbers to MPFR require explicit Precision; configuration bindings therefore resolve runtime-precision inputs as strings. Exact fractions can be displayed as fractions or rounded decimals without changing the stored value. Table JSON preserves exactness/precision tags; the current metadata string-map export does not. See typed data tables and run contexts for the conversion and export contracts. format_real(mpreal) remains available for explicit textual output.

Trace precision is independently configurable for float32, float64, and float128 values. The global environment variables are UNI20_FP_PRECISION_FLOAT32, UNI20_FP_PRECISION_FLOAT64, and UNI20_FP_PRECISION_FLOAT128; append _MODULE_<MODULE> for a module-specific override. Complex values use the precision of their real component type.

Numeric Limits

Scalar-generic Uni20 algorithms should use uni20::numeric_limits<T> rather than naming std::numeric_limits<T> directly. The primary Uni20 template is undefined: there are no generic default values. A constrained specialization explicitly delegates native arithmetic types to their specialized standard-library limits. Every library scalar must supply its own Uni20 specialization; a standard specialization alone does not opt a library type into the Uni20 interface.

Half-integer limits are defined by common/half_int.hpp: numeric_limits<basic_half_int<T>> describes an exact, bounded, signed scalar with radix 2 and the value-bit count of its doubled integer representation. is_integer is false because values may have a fractional half. lowest() and max() return exact half-integer endpoints. Infinity and NaN capabilities are false; epsilon, floating-point exponent bounds, and an ambiguous min() query are not provided. uni20::is_exact(half_int_value) consequently returns true.

Generic code must establish the numeric category before querying its limits. An ordinary && expression does not prevent template instantiation of its right-hand side. Use nested if constexpr branches or constraints, and probe optional quantities with requires. In particular, storage initialization must not instantiate numeric limits for strings or other nonnumeric element types.

For extension or library scalar types, specialize uni20::numeric_limits<T>:

namespace uni20
{
template <> struct numeric_limits<my_real>
{
static constexpr bool is_specialized = true;
static constexpr bool is_exact = false;
static constexpr int digits() { return /* ... */; }
static constexpr int digits(my_real const&) { return digits(); }
static my_real epsilon();
static my_real epsilon(my_real const&); // Same value, for generic callers.
static my_real min();
static my_real max();
};
} // namespace uni20
Definition async/async.hpp:26
constexpr bool is_exact(detail::standard_complex< T > const &x)
A standard complex value is exact only when both components are exact.
Definition math.hpp:50
mpreal epsilon(Precision precision)
Spacing above one for a specified binary working precision.
Definition mpreal.hpp:805

Do not add specializations of std::numeric_limits for compiler fundamental extension types such as __float128 or _Float128. Those are not user-defined types. Keep such support behind the Uni20 customization point.

Runtime precision

numeric_limits<mpreal> separates fixed type properties (radix, is_exact, is_integer, etc.) from quantities requiring a working precision:

auto p = Precision::bits(256);
mpreal x("0.1", p);
auto eps = numeric_limits<mpreal>::epsilon(x); // 2^(1 - 256), at 256 bits
auto bits = numeric_limits<mpreal>::digits(x);
auto same_eps = numeric_limits<mpreal>::epsilon(p);

Scalar-generic code uses numeric_limits<Real>::digits(value) and epsilon(value), or the corresponding queries taking Precision. Native types also provide zero-argument digits(), digits10(), max_digits10() and epsilon(); these remain constexpr. Decimal digit queries on native types also accept an exemplar or precision. Explicit precision must match the native type, including when querying limits. Fixed properties such as radix, is_integer and max_exponent remain constants. MPFR has no precisionless digit count or epsilon; decimal digit limits are not currently part of its specialization.

An exact or unset exemplar cannot supply finite working precision: these runtime queries throw rather than return zero or choose an ambient default. Pass a finite Precision explicitly when starting an approximate algorithm from exact inputs. numeric_limits<mpreal>::epsilon() is deleted, and other unsupported limits are absent. has_numeric_limits_v indicates a specialization exists, not that every query is available without a value. Generic algorithms requiring type-wide limits must constrain those particular queries. This prevents the unspecialized standard template's zero-valued fallback from silently becoming a convergence tolerance.

Exactness queries

uni20::is_exact(value) queries the active arithmetic state of mpreal, complex<mpreal> and exact_rational, and the static exactness of ordinary scalar types. It returns false for an unset runtime scalar. numeric_limits<T>::is_exact remains a compile-time type property; it cannot describe the changing state of an individual mpreal. Ordinary integers are exact, while native floating values are approximate even when numerically integral. Exact rational roots, powers, parsing and explicit native conversions are described in mpreal.

Mixed exact/approximate basic arithmetic retains exact operands until rounding the result to the finite operand's precision. Finite +, -, * and / results are correctly rounded (per component for complex values); division requires a nonzero divisor for this statement. Mixed complex multiplication and division, and exact-real/approximate-real division, keep large binary exponents separate from rational coefficients. The large-exponent path uses exact scaled comparisons to select the rounded result, including cancellation and ties; temporary integer storage scales with significand and rational sizes rather than the numerical exponent. Ordinary-sized exponents use direct rational arithmetic. Two approximate operands still require matching precision. Approximation-driven algorithms must obtain finite working precision explicitly or from their inputs; an all-exact state supplies no approximation budget.

Runtime-precision real and complex elementary functions accept a trailing finite Precision: they convert each input to that precision, then evaluate, always returning an approximate result at that precision. Without an override, exact results remain exact where supported, and approximate operands supply the working precision. This distinction also applies to the real outputs of complex abs, norm, and arg. Real expm1/log1p use dedicated provider routines; new elementary functions follow the same exact/finite-precision distinction. Real arithmetic utilities instead round the result from the original operands when given an explicit precision: this covers integer powers/roots, integer rounding, fused arithmetic, extrema, remainders, decomposition and binary scaling. Sign sources and neighbor directions do not supply result precision; adjacent-value operations require a finite grid. Paired trig/hyperbolic functions retain the elementary input-conversion contract. MPFR special functions and pi-scaled/small-argument extensions use that same contract, except that compound is an arithmetic utility and rounds the result. Missing native implementations remain unavailable; enabling MPFR does not add MPFR adapters for float32/64/80/128. Native log-Gamma and integer-order Bessel functions use independently detected platform routines at each precision, including reentrant sign handling and checked conversion of Bessel orders. See native special functions. See the real operation and exact-result tables and coverage plan, and special-function contracts.

Numerical validation

Scalar support is validated per operation and backend using the shared numerical precision cases. Float80 and finite MPFR/MPC values participate alongside float32/64/128; MPFR runs at both 128 and 256 bits. Unsupported algorithms and missing optional providers are reported explicitly. Passing scalar arithmetic or one backend's tests does not establish library-wide support for that scalar.