diff --git a/docs/redesign/DESIGN.md b/docs/redesign/DESIGN.md index 3e5302d..0f7546f 100644 --- a/docs/redesign/DESIGN.md +++ b/docs/redesign/DESIGN.md @@ -1,7 +1,7 @@ # Numerixx 2 — Design Reference - **Date:** 2026-09-27 -- **Status:** Approved on 2026-09-28, with every §12 default accepted. The phase-1 core design was approved on 2026-10-04 (§12.20) and is not built yet. +- **Status:** Approved on 2026-09-28, with every §12 default accepted. The phase-1 core design was approved on 2026-10-04 (§12.20), revised after a simplicity review on 2026-10-06 (§12.21), and is not built yet. - **Scope:** every module of Numerixx (core, deriv, poly, roots, optimize, linalg + multiroots, integrate, interpolate), the build system, tests and CI, upstream work in FXT, migration from Numerixx 1.x, and the families planned after v2.0 (§1.1, §10.5). **Purpose.** This is the detailed companion to [`docs/redesign/PLAN.md`](PLAN.md), the short plan. It records the library's scope and positioning, every design decision with its rationale and the alternatives considered, the principles behind them, the build and dependency design, the core abstractions with code sketches, the per-module algorithm tables (including the bugs that must not be ported), the testing and CI design, the roadmap with phase sizes and acceptance criteria, the risks, and the decisions that were open until 2026-09-28, with their defaults (all accepted). Section numbers are stable, so other documents and code comments can cite them (for example "DESIGN §6.8"). Requirement IDs (R-B1, R-E3, R-A5, …) are listed in Appendix B. @@ -10,7 +10,7 @@ - **[prototyped]**: compiled and run in the feasibility prototype preserved at [`docs/redesign/prototype/`](prototype/), on 9 configurations with bit-identical output: GCC 16.1 and Clang 22 + libc++, each with and without `-fno-exceptions`; em++ 6.0.8 with `-fexceptions`, `-fno-exceptions` and `-fwasm-exceptions`; MSVC 19.51; clang-cl 22. MSVC and clang-cl also passed with `/EHs-c-`. - **[prototyped mechanism]**: the prototype verified the mechanism, but the spelling here differs (the prototype predates some renames). - **[sketch]**: not yet compiled. Spike exit criteria 7–11 (§10.2) make the important sketches concrete first. -- **[phase 1, approved 2026-10-04; not built]**: decided with the phase-1 core design (§12.20), but not yet in the code. Where today's code still behaves the old way, the text says so. Measurements cited for such items come from the reviewers' prototypes and probes of that design, not from the library. +- **[phase 1, approved 2026-10-04; not built]**: decided with the phase-1 core design (§12.20), revised on 2026-10-06 (§12.21), but not yet in the code. Where today's code still behaves the old way, the text says so. Measurements cited for such items come from the reviewers' prototypes and probes of that design, not from the library. The prototype is throwaway evidence, not library code. It still contains an in-house constexpr LU (`nxx/linalg.hpp`) and zero-heap design choices that this design no longer requires. Claims that the prose calls *verified* or *measured* without a bracketed marker come from exploratory work that is not preserved: build and CMake experiments, the linear-algebra research, exploratory solver prototypes, and numerical and usability probes of a simpler predecessor API. Where the text says "a naive design would …", the figure quoted was measured on that predecessor. @@ -209,7 +209,7 @@ Status legend: **v2.0** = in the first release; **planned** = scheduled in §10. | Tier | Mechanism | Examples | Run-time cost | |---|---|---|---| -| **A. Compile time** | types, concepts, `consteval` literal constructors, deleted overloads with reasons | Invalid literals: `bracket{2.0, 1.0}`, `tolerance{-1e-8}`, `max_iterations m = 0` and `= true`, `x_tol{0.0, nxx::rel_tolerance{0.0}}` (today `x_tol{0.0, 0.0}`). Solver/input mismatches: bisection on a guess; Newton without a derivative source; a fixed-size guess of the wrong length; `quadratic{0.0, 1.0, 2.0}`. Criterion/solver mismatches: **`x_tol` or `step_tol` on a bracketing solver**; `width_tol` on an open method; **`f_tol` on a minimiser**. Composition errors: `first_of` over mismatched result types; `then(newton, brent)`, where a bracket solver cannot take a point estimate without `.from_enclosure()` (phase 3; it does not exist in the code yet); an `any_solver` built from a solver whose result type differs. Roles: `rel_tolerance` where `tolerance` is expected; two bare numbers in `x_tol{a, b}` or `width_tol{a, b}`, and a relative part alone, `width_tol{rel}` (§6.2) **[phase 1, approved 2026-10-04; not built]**. **[prototyped: 16 compile-fail tests on 5 compilers]** | none | +| **A. Compile time** | types, concepts, `consteval` literal constructors, deleted overloads with reasons | Invalid literals: `bracket{2.0, 1.0}`, `tolerance{-1e-8}`, `max_iterations m = 0` and `= true`, `x_tol{0.0, nxx::rel_tolerance{0.0}}` (today `x_tol{0.0, 0.0}`). Solver/input mismatches: bisection on a guess; Newton without a derivative source; a fixed-size guess of the wrong length; `quadratic{0.0, 1.0, 2.0}`. Criterion/solver mismatches: **`x_tol` or `step_tol` on a bracketing solver**; `width_tol` on an open method; **`f_tol` on a minimiser**. Composition errors: `first_of` over mismatched result types; `then(newton, brent)`, where a bracket solver cannot take a point estimate without `.from_enclosure()` (phase 3; it does not exist in the code yet); an `any_solver` built from a solver whose result type differs. Roles: `rel_tolerance` where `tolerance` is expected; two bare numbers in `x_tol{a, b}` or `width_tol{a, b}`, and a part alone, `width_tol{rel}` or `width_tol{abs_tolerance{a}}` (§6.2) **[phase 1, approved 2026-10-04; not built]**. **[prototyped: 16 compile-fail tests on 5 compilers]** | none | | **B. Construction time** | private constructors + `make() → std::expected` | a tolerance or budget from a config file; a bracket from run-time values; `sign_bracket::make(f, b)`; strictly increasing knots; finite polynomial coefficients | one check, once, at the boundary | | **C. Run time** | the error channel of every solver; unvalidated inputs accepted by solvers | braced `{lo, hi}`, `std::pair` and `expected` inputs validated in `prepare()`; NaN from the callback; the callback's own error; zero derivative; singular Jacobian; stall; cycling; divergence; budget exhausted; a sign change that is a pole; a run-time dimension mismatch between a system and its guess | once per evaluation or step | @@ -233,7 +233,7 @@ Rules: - **Real scalars:** `float`, `double` and `long double` in every test; `cpp_bin_float_50` in the multiprecision leg. - **Maths calls** use the ADL idiom (`using std::abs; abs(x)`) or the thin `nxx::math` helpers that wrap it, so the functions of a multiprecision type, which live in its own namespace, are found. -- **Defaults are functions of `T`:** `rel = math::root_eps(1, 2)` and similar. A `static_assert` test instantiates every module default for `float`, `double` and `long double` and checks that it is achievable (`default_rel >= 4·eps_T`). `cpp_bin_float_50` is not a literal type, so its values are not constant expressions (measured on GCC 16.1, Clang 22.1.8, MSVC 19.51 and clang-cl 22.1.3 by the C++ review of the phase-1 core note); for it the test checks the property with constexpr integer arithmetic on `std::numeric_limits::digits`, plus a run-time check of the thresholds (§10.3 phase 1, A1) **[phase 1, approved 2026-10-04; not built]**. +- **Defaults are functions of `T`:** `rel = math::root_eps(1, 2)` and similar. A `static_assert` test instantiates every module default for `float`, `double` and `long double` and checks that it is achievable (`default_rel >= 4·eps_T`). `cpp_bin_float_50` is not a literal type, so its values are not constant expressions (measured on GCC 16.1, Clang 22.1.8, MSVC 19.51 and clang-cl 22.1.3 by the C++ review of the phase-1 core note); for it a run-time check in the multiprecision test binary (`gcc-multiprecision` preset) calls the library's own `floored_width::factor()` and `step_tol::threshold` (`threshold(1)` and `threshold(0)` for `step_tol<3,5>` and `step_tol<7,10>`) and compares them with 4·eps computed at run time, because `four_eps`, a constexpr variable template, cannot be instantiated for it (§10.3 phase 1, A1) **[phase 1, approved 2026-10-04; not built]**. No such check for `cpp_bin_float_50` exists today, so this adds one; the `static_assert`s for the three built-in types stay where they are, inside the `TEST_CASE_TEMPLATE` of `tests/core/test_criteria.cpp`. Revised on 2026-10-06 (§12.21): the approved constexpr integer arithmetic on `std::numeric_limits::digits` copied the library's formula instead of testing it, and the copy in the phase-1 core note was off by one. - **Multiprecision:** `cpp_bin_float_50` satisfies `real` with no adapter. Only expression-template-off types are supported: algorithms write `T x = …`, never `auto x = a - b;`. The same rule protects against Eigen's expression templates (§5.3). - **AD scalars are not a design goal** and are not tested. A user type that satisfies `real` (which needs a specialised `std::numeric_limits`, §6.1) is accepted, but control flow compares values of `T` directly, and nothing guarantees convergence of derivative parts. - **Complex:** `poly` only. @@ -271,7 +271,7 @@ Rules: - **Diagnostics** (claims limited to what tests enforce, §9.1): - Contract violations of combinators and solver inputs produce a `static_assert` or a reasoned deletion whose message is in the **first** error on GCC and Clang. - The `first_of` mismatch is a single error line on GCC and MSVC **[prototyped]**. That is the `static_assert` form, which the combinators still use. - - **Combinators move to reasoned deletions [phase 1, approved 2026-10-04; not built].** `first_of_t`, `then_t` and `warm_fallback_t` get a constrained call operator and one deleted sibling per misuse (§6.10), so `std::is_invocable_v` is false for every misuse; today the `static_assert` sits in the body of a call operator with a deduced return type, so asking `std::is_invocable_v` instantiates the body and fails there. The cost, accepted on 2026-10-04: GCC 14 and cl no longer print the text. GCC 14 shows "use of deleted function … declared here", and cl C2280 ("attempting to reference a deleted function") at the declaration's line, which holds the reason (`NXX_DELETE` starts on the declarator's line, §5.3). The single-line mismatch above then becomes the deleted sibling's error; its line counts are re-measured when the change is built (Appendix D). + - **Combinators move to reasoned deletions [phase 1, approved 2026-10-04; not built].** `first_of_t`, `then_t` and `warm_fallback_t` get a constrained call operator and a reasoned deleted sibling for each kind of misuse, 8 in all (§6.10; 18 as approved on 2026-10-04, before the revision of 2026-10-06, §12.21), so `std::is_invocable_v` is false for every misuse; today the `static_assert` sits in the body of a call operator with a deduced return type, so asking `std::is_invocable_v` instantiates the body and fails there. The cost, accepted on 2026-10-04: GCC 14 and cl no longer print the text. GCC 14 shows "use of deleted function … declared here", and cl C2280 ("attempting to reference a deleted function") at the declaration's line, which holds the reason (`NXX_DELETE` starts on the declarator's line, §5.3). The single-line mismatch above then becomes the deleted sibling's error; its line counts are re-measured when the change is built (Appendix D). - MSVC does not print deletion reasons (it rejects `= delete("…")`); it shows the deleted declaration, whose source line holds the reason. - Symbol length: the longest mangled name in the core TU is 381 characters, about 1.37k demangled **[prototyped]**. @@ -632,8 +632,6 @@ template class sign_bracket; // §6.6 template inline constexpr bool is_bare_number_v = std::is_arithmetic_v> || real>; template using bare_scalar_t = std::conditional_t>, std::remove_cvref_t, double>; template inline constexpr bool is_rel_v = std::same_as, rel_tolerance>; - template inline constexpr bool is_any_rel_v = false; // refined - template inline constexpr bool is_any_rel_v> = true; template inline constexpr bool is_tolerance_v = false; // tolerance (brent{*tol}, §7.2) template inline constexpr bool is_tolerance_v> = true; template inline constexpr bool is_tolerance_part_v = false; // abs_tolerance, rel_tolerance @@ -652,56 +650,56 @@ template class sign_bracket; // §6.6 template requires(detail::is_bare_number_v && detail::is_bare_number_v) width_tol(A, B) NXX_DELETE("say which number is relative: width_tol{1e-10, nxx::rel_tolerance{1e-8}}; purely relative: " "width_tol{0.0, nxx::rel_tolerance{1e-8}}; one number is absolute: width_tol{1e-10}"); - template requires detail::is_any_rel_v> - width_tol(R) NXX_DELETE("a relative part needs an absolute part: width_tol{0.0, nxx::rel_tolerance{r}} is purely relative"); + template requires detail::is_tolerance_part_v> // abs_tolerance or rel_tolerance + width_tol(R) NXX_DELETE("a part alone is not a criterion: width_tol{a} is absolute (width_tol::make(a) at run time); " + "width_tol{0.0, nxx::rel_tolerance{r}} is purely relative (make(0.0, *rel) at run time)"); static constexpr auto make(T abs) noexcept -> std::expected; // finite and > 0, as tolerance::make + template requires detail::is_rel_v // the run-time mirror of the literal + static constexpr auto make(T abs, R r) noexcept -> std::expected // [revised 2026-10-06, §12.21] + { if (!detail::mixed_tolerance_ok(abs, r.value())) return std::unexpected(errc::invalid_input); // abs finite, >= 0 + return width_tol{detail::trust_me{}, abs, r.value()}; } // never through make(T, T): it is deleted template requires detail::is_rel_v static constexpr auto make(abs_tolerance a, R r) noexcept -> std::expected - { if (!detail::mixed_tolerance_ok(a.value(), r.value())) return std::unexpected(errc::invalid_input); - return width_tol{detail::trust_me{}, a.value(), r.value()}; } // never through make(T, T): it is deleted + { return make(a.value(), r); } static void make(T, T) NXX_DELETE("say which number is relative: make(abs_tolerance, rel_tolerance); " "make(abs) for an absolute tolerance"); - template requires detail::is_rel_v - static void make(T, R) NXX_DELETE("validate the absolute part too: make(*nxx::abs_tolerance::make(a), rel); " - "for literals width_tol{a, nxx::rel_tolerance{r}}"); }; template width_tol(T) -> width_tol; template requires detail::is_bare_number_v width_tol(A, rel_tolerance) -> width_tol; template width_tol(abs_tolerance, rel_tolerance) -> width_tol; template requires(detail::is_bare_number_v && detail::is_bare_number_v) width_tol(A, B) -> width_tol>; // so the deleted constructor reports, not CTAD - template width_tol(rel_tolerance) -> width_tol; // likewise + template requires detail::is_tolerance_part_v + width_tol(R) -> width_tol; // likewise, for either part ``` - **Spellings.** Absolute: `width_tol{1e-10}`. Mixed: `width_tol{1e-10, nxx::rel_tolerance{1e-8}}`. Purely relative: `width_tol{0.0, nxx::rel_tolerance{1e-8}}`, legal because `abs_tolerance` accepts 0. Every spelling builds the same `(abs, rel)` pair as today's positional form, so no criterion's test changes (checked by the numerics review of the phase-1 core note). - - **Run-time values.** `make(abs)` takes an absolute tolerance in one check, and fails with `invalid_input` for abs ≤ 0, NaN or ±inf. A mixed tolerance takes three checks: + - **Run-time values.** `make(abs)` takes an absolute tolerance in one check, and fails with `invalid_input` for abs ≤ 0, NaN or ±inf. A mixed tolerance takes two checks, because `make(T, rel_tolerance)`, the run-time mirror of the literal `width_tol{a, nxx::rel_tolerance{r}}`, validates the absolute part in-band (revised on 2026-10-06, §12.21; approved on 2026-10-04 as a deletion, "validate the absolute part too", with three checks): ```cpp if (auto w = nxx::width_tol::make(t)) r::brent{*w}(f, {lo, hi}); // absolute: one check - auto A = nxx::abs_tolerance::make(a); // mixed: three checks - auto R = nxx::rel_tolerance::make(r); - if (!A || !R) return /* invalid_input */; - auto w = nxx::width_tol::make(*A, *R); - if (!w) return /* abs == 0 && rel == 0 */; - auto p = nxx::width_tol::make(nxx::abs_tolerance{0.0}, *R); // purely relative at run time + auto R = nxx::rel_tolerance::make(r); // mixed: two checks + if (!R) return /* invalid_input */; + auto w = nxx::width_tol::make(a, *R); + if (!w) return /* invalid_input: a negative or non-finite a, or a == 0 && rel == 0 */; + auto p = nxx::width_tol::make(0.0, *R); // purely relative at run time ``` - `rel_tolerance` rejects rel ≥ 1. Not added: `make(rel_tolerance)` (the purely relative form above covers it), a `make` over two `std::expected` parts, and a reasoned deletion for an unchecked `make()` result passed to a solver (`brent{width_tol::make(t)}`). That last one stays the compiler's own error, 118 lines on GCC 16 and 67 on Clang 22 (measured by the API-ergonomics review of the phase-1 core note), and is a candidate for phase 3. + `make(a, *R)` checks exactly what the three-step path through `abs_tolerance::make` checks, with the same code (`invalid_input`); `make(abs_tolerance, rel_tolerance)` stays and delegates to it. `make(T, T)` stays deleted, so the relative part is always typed and the roles cannot be swapped. `rel_tolerance` rejects rel ≥ 1. Not added: `make(rel_tolerance)` (the purely relative form above covers it), a `make` over two `std::expected` parts, and a reasoned deletion for an unchecked `make()` result passed to a solver (`brent{width_tol::make(t)}`). That last one stays the compiler's own error, 118 lines on GCC 16 and 67 on Clang 22 (measured by the API-ergonomics review of the phase-1 core note), and is a candidate for phase 3. - **Rejected spellings and what the caller sees:** - `width_tol{1e-10, 1e-8}`, `x_tol{a, b}` (run-time doubles too), `width_tol{1e-20, 0}`, `x_tol{0.0, 1e-8}`: the two-number reason; - `make(a, b)` with numbers: the deleted `make(T, T)`; - - `make(a, *rel)` with a run-time `a`: "validate the absolute part too"; - - `width_tol{rel}`, as a literal or at run time: the relative-alone reason (at run time, use `make(abs_tolerance{0.0}, *r)`); + - a part alone, `width_tol{rel}` or `width_tol{nxx::abs_tolerance{a}}`, as a literal or at run time: the part-alone reason, which names both run-time paths (`make(a)`, `make(0.0, *rel)`); the second is a CTAD error with no reason today and gains it (revised on 2026-10-06, §12.21: the deletion is keyed on `is_tolerance_part_v`, so no `is_any_rel_v` trait); - `width_tol{0.0, nxx::rel_tolerance{0.0}}`: the consteval invariant text; `width_tol{-1e-10, nxx::rel_tolerance{1e-8}}`: `abs_tolerance`'s literal check; - swapped roles, `width_tol{nxx::rel_tolerance{1e-8}, 1e-10}` and `width_tol{1e-10, nxx::abs_tolerance{1e-8}}`: the compiler's own CTAD error; - `width_tol{*a, *r}` with run-time parts: "not a constant expression" (C7595 on cl): use `make`; - `make(*abs, 0.5)`, the relative part as a number: the compiler's own error; - `width_tol{1e-10f, nxx::rel_tolerance{1e-8}}` is accepted, but Clang warns at the call site with `-Wdouble-promotion`: write both parts in one type. - - **Measured on prototypes** by the C++ and API-ergonomics reviews of the phase-1 core note (GCC 16.1, Clang 22.1.8, MSVC 19.51, clang-cl 22.1.3): every CTAD form works; both deletions put their reason in the first error on GCC, Clang and clang-cl, and cl points to the declaration; swapped roles fail with the compiler's CTAD error; the mixed-type `float` spelling warns on Clang, while GCC 16 and cl are silent. + - **Measured on prototypes** by the C++ and API-ergonomics reviews of the phase-1 core note (GCC 16.1, Clang 22.1.8, MSVC 19.51, clang-cl 22.1.3): every CTAD form works; both deletions put their reason in the first error on GCC, Clang and clang-cl, and cl points to the declaration (the part-alone deletion in its approved form, keyed on `rel_tolerance` only); swapped roles fail with the compiler's CTAD error; the mixed-type `float` spelling warns on Clang, while GCC 16 and cl are silent. - **Texts that quote the old form** change with it, and the compile-fail EXPECT prefixes stay: the reasons in §6.8 (`x_tol{abs}`, `x_tol{abs, nxx::rel_tolerance{rel}}`, and the same for `width_tol`), the comments in `criteria.hpp` and `brent.hpp`, and the quick tour. - - **Tests.** New compile-fail cases, all with `DELETE_REASON`: `width_tol_two_numbers`, `x_tol_two_numbers` and `width_tol_make_two_numbers` ("say which number is relative"), `width_tol_make_runtime_abs` ("validate the absolute part too"), `width_tol_relative_alone` ("a relative part needs an absolute part"). Rewritten: `x_tol_zero_zero` (case `x_tol{0.0, nxx::rel_tolerance{0.0}}`, control `x_tol{0.0, nxx::rel_tolerance{1e-8}}`) and `rel_tolerance_as_tolerance`, which now reaches the deleted constructor, so its EXPECT becomes that reason plus `DELETE_REASON`. Negative requires-tests go through concepts (a negative requires-expression outside a template is ill-formed, not false). The sites that use the two-number literal or `make(T, T)` move to the new spellings; the golden-table rows among them change their code but keep their labels (compared as strings) and their values. + - **Tests.** New compile-fail cases, all with `DELETE_REASON`: `width_tol_two_numbers`, `x_tol_two_numbers` and `width_tol_make_two_numbers` ("say which number is relative"), `width_tol_relative_alone` ("a part alone is not a criterion"). The approved case `width_tol_make_runtime_abs` is dropped with its deletion (§12.21): a doctest case checks that `make(a, *rel)` succeeds for a valid `a` and returns `invalid_input` for a negative one. Rewritten: `x_tol_zero_zero` (case `x_tol{0.0, nxx::rel_tolerance{0.0}}`, control `x_tol{0.0, nxx::rel_tolerance{1e-8}}`) and `rel_tolerance_as_tolerance`, which now reaches the deleted constructor, so its EXPECT becomes that reason plus `DELETE_REASON`. Negative requires-tests go through concepts (a negative requires-expression outside a template is ill-formed, not false). The sites that use the two-number literal or `make(T, T)` move to the new spellings; the golden-table rows among them change their code but keep their labels (compared as strings) and their values. - A chain-wide budget is `with_evaluation_budget` (§6.10); there is no per-solver "remaining budget" arithmetic. -- **FLAG (portability).** MSVC 19.51 lacks P2564. A user template such as `template constexpr auto make_solver(T t) { return r::bisection{nxx::width_tol{t}}; }` escalates to an immediate function and compiles on GCC, Clang and clang-cl, but fails on MSVC with C7595 **[verified in the spike: tests/compile_fail/probe_p2564_escalation.cpp]**; only constexpr functions escalate (P2564), so without `constexpr` the call is an error on every compiler. Rule: *every function that forwards a tolerance or budget takes the refined type (`nxx::tolerance`, `nxx::max_iterations`), never a raw scalar; wrap run-time values with `make()`*. On the MSVC leg the probe must fail with C7595 as its first error, which documents the gap. **Mixed criteria [phase 1, approved 2026-10-04; not built]:** forwarding refined values works for the absolute form, because `width_tol(tolerance)` is constexpr. The role-typed mixed constructor above is consteval, so a constexpr template that forwards `abs_tolerance` and `rel_tolerance` into it compiles on GCC 16.1, Clang 22.1.8 and clang-cl 22.1.3 and fails on cl 19.51 with C7595 (measured on a prototype by the C++ review of the phase-1 core note). Generic code that forwards a mixed tolerance therefore calls `width_tol::make(abs_tolerance, rel_tolerance)`, and so does library code. The P2564 probe gains that refined-forwarding case. Also, `std::is_constructible_v, double>` is **true** on all five compilers, so generic code must detect validated inputs with `nxx::is_refined_v`, not `constructible_from` or `convertible_to` **[prototyped]**. +- **FLAG (portability).** MSVC 19.51 lacks P2564. A user template such as `template constexpr auto make_solver(T t) { return r::bisection{nxx::width_tol{t}}; }` escalates to an immediate function and compiles on GCC, Clang and clang-cl, but fails on MSVC with C7595 **[verified in the spike: tests/compile_fail/probe_p2564_escalation.cpp]**; only constexpr functions escalate (P2564), so without `constexpr` the call is an error on every compiler. Rule: *every function that forwards a tolerance or budget takes the refined type (`nxx::tolerance`, `nxx::max_iterations`), never a raw scalar; wrap run-time values with `make()`*. On the MSVC leg the probe must fail with C7595 as its first error, which documents the gap. **Mixed criteria [phase 1, approved 2026-10-04; not built]:** forwarding refined values works for the absolute form, because `width_tol(tolerance)` is constexpr. The role-typed mixed constructor above is consteval, so a constexpr template that forwards `abs_tolerance` and `rel_tolerance` into it compiles on GCC 16.1, Clang 22.1.8 and clang-cl 22.1.3 and fails on cl 19.51 with C7595 (measured on a prototype by the C++ review of the phase-1 core note). Generic code that forwards a mixed tolerance therefore calls `width_tol::make(abs_tolerance, rel_tolerance)` (or, since the revision of 2026-10-06, `make(T, rel_tolerance)`), and so does library code. The P2564 probe gains that refined-forwarding case. Also, `std::is_constructible_v, double>` is **true** on all five compilers, so generic code must detect validated inputs with `nxx::is_refined_v`, not `constructible_from` or `convertible_to` **[prototyped]**. ### 6.3 Error and result types [prototyped] @@ -742,24 +740,20 @@ template struct failure { // trivially copyable wh template using result = std::expected, failure>; template requires requires(const R& r) { r->x; } // not for a search result: a sign_bracket has two ends [spike] constexpr auto best_x(const R& r); // optional: the solution's x, or the failure's best->x [prototyped] + // doc comment: a search result has no x: read r->lo() and r->hi() + // [phase 1, approved 2026-10-04; not built] (no deleted sibling: §12.21) -// [phase 1, approved 2026-10-04; not built] +// [phase 1, approved 2026-10-04; not built] (revised on 2026-10-06, §12.21) namespace detail { - template inline constexpr bool is_result_v = false; // also used by the combinators' classifiers (§6.10) + template inline constexpr bool is_result_v = false; // used by the combinators' classifiers (§6.10) template inline constexpr bool is_result_v, failure>> = true; - template inline constexpr bool same_estimate_v = false; - template inline constexpr bool same_estimate_v, failure>> = true; } -template requires detail::same_estimate_v // the solution's estimate, or the failure's best -[[nodiscard]] constexpr auto best(const R& r) -> std::optional -{ using Est = typename R::error_type::estimate_type; - if (r) return std::optional(static_cast(*r)); return r.error().best; } -template requires(detail::is_result_v && !detail::same_estimate_v) -void best(const R&) NXX_DELETE("nxx::best: this result succeeds and fails with different estimates (a search result: a " - "sign_bracket, then a root_estimate): read *r and r.error().best separately"); -template requires(detail::is_result_v && !requires(const R& r) { r->x; }) -void best_x(const R&) NXX_DELETE("nxx::best_x: this result's estimate has no x: for a search result read r->lo() and " - "r->hi(); for other estimates use nxx::best(r)"); +template // the solution's estimate, or the failure's best +[[nodiscard]] constexpr auto best(const std::expected, failure>& r) -> std::optional +{ if (r) return std::optional(static_cast(*r)); return r.error().best; } +template requires(!std::is_same_v) +void best(const std::expected, failure>&) NXX_DELETE("nxx::best: this result succeeds and fails with " + "different estimates (a search result: a sign_bracket, then a root_estimate): read *r and r.error().best separately"); } namespace nxx::roots { template struct root_estimate { @@ -782,7 +776,7 @@ template struct root_estimate { Every 1-D root solver applied to the same f returns exactly the same type, so chains type-check. One-shot differentiation (`diff`, `central`) returns `expected>` (§6.12). - **One name per quantity [phase 1, approved 2026-10-04; not built].** A result's algorithm is `res ? res->by : res.error().by`, and a fault's cost is `fault::evaluations`, the name `counters` uses. Construction is positional at every site (`Fail{code, id, …}`), so only the readers of `.where` and `.evals` change. These are Numerixx 2 alpha spellings, so the CHANGELOG lists the renames and `MIGRATION.md` gets no row. The API-ergonomics review of the phase-1 core note measured no `-Wshadow` warning from the library's locals named `best` once `nxx::best` exists (GCC 16, Clang 22). -- **`nxx::best(r)` [phase 1, approved 2026-10-04; not built]** returns the solution's estimate or the failure's best estimate as one `std::optional`, for every result whose success and failure carry the same estimate type, through `first_of` and `any_solver` too. A search result succeeds with a `sign_bracket` and fails with a `root_estimate`, so `best` is deleted for it with a reason, and `best_x`, which today is only constrained away there, gets a reasoned deletion as well. A one-shot derivative's `expected` is not a result, so `best` is constrained away for it with no reason. Tests: compile-fail `best_search_result` (EXPECT "succeeds and fails with different estimates") and `best_x_search_result` (EXPECT "this result's estimate has no x"), both with `DELETE_REASON`; concept tests for `best` on a root result (true), on a search result and on `std::expected>` (false), and for `best_x` on a search result (false). +- **`nxx::best(r)` [phase 1, approved 2026-10-04; not built]** returns the solution's estimate or the failure's best estimate as one `std::optional`, for every result whose success and failure carry the same estimate type, through `first_of` and `any_solver` too. A search result succeeds with a `sign_bracket` and fails with a `root_estimate`, so `best` is deleted for it with a reason. `best` deduces `std::expected, failure>` directly, and its deleted sibling takes `solution` with S ≠ E, so it needs no trait of its own (revised on 2026-10-06, §12.21, in place of the approved `same_estimate_v`; the behaviour is the same on GCC 16.1, Clang 22.1.8, cl 19.51 and clang-cl 22.1.3, measured on probes by the simplicity review). A one-shot derivative's `expected` is not a result, so `best` is constrained away for it with no reason. `best_x` keeps today's constraint and gets no deleted sibling (revised on 2026-10-06, §12.21; approved on 2026-10-04 with one): the compiler's message names the missing `x` (GCC: "the required expression 'r->x' is invalid"), and `best_x`'s doc comment names the remedy, "a search result has no x: read r->lo() and r->hi()". Tests: compile-fail `best_search_result` (EXPECT "succeeds and fails with different estimates", `DELETE_REASON`); concept tests for `best` on a root result (true), on a search result and on `std::expected>` (false), and for `best_x` on a search result (false). - **Input codes [phase 1, approved 2026-10-04; not built].** - `non_finite_input`: every NaN or ±inf input value, with cost {0, 0} and no best estimate. That covers a bracket end (checked in `bracket::make`, which braced, pair and window inputs also go through), a guess or a root estimate's x, a projected start, and the x of `diff` (so also of `derivative_of(f)(x)`). - `invalid_input`: equal ends; a derivative step h that vanishes or overflows at a finite x; a stencil point that overflows at a finite x when `diff` is called directly. @@ -834,29 +828,18 @@ template constexpr bool is_fatal(const E&) noexcept; // CPO, if constexpr (!std::is_invocable_v) return false; else return callback_value_ok_v, X>; }(); - // From converts to To without narrowing: built-in floating types only (otherwise true), by list-initialisation. - template inline constexpr bool value_fits_v = [] { - if constexpr (std::is_floating_point_v && std::is_floating_point_v) return requires(From v) { To{v}; }; - else return true; - }(); - template inline constexpr bool narrows_float_v = - std::is_floating_point_v && std::is_floating_point_v && !value_fits_v; - template constexpr X to_scalar(V&& v) { - using W = std::remove_cvref_t; - if constexpr (std::is_arithmetic_v && std::is_arithmetic_v) return static_cast(v); // no C4244, no -Wfloat-conversion - else if constexpr (std::integral) return X(v); // as the library's own T(0) - else { X y = std::forward(v); return y; } - } - } + template constexpr X to_scalar(const V& v) { return static_cast(v); } // no C4244, no -Wfloat-conversion + } // [revised 2026-10-06, §12.21] // evaluate_impl, on the evaluate and the evaluate_sample path alike: // const W v = ; const X y = detail::to_scalar(v); - // if constexpr (detail::narrows_float_v) + // if constexpr (std::is_floating_point_v && std::is_floating_point_v && !std::is_same_v) // if (math::isfinite(v) && (!math::isfinite(y) || (y == X(0) && v != W(0)))) // return R{std::unexpect, fault{errc::non_finite_value, cost_of(fn), {}}}; ``` - - **Results.** f returns a real or an integer, or `std::expected` of one. Rejected with a reason, and `std::is_invocable_v` false: `bool`, `void`, `std::optional`, a string, an expression-template result (D16: only expression-template-off types are supported), a proxy or implicitly convertible class, and a type that converts to the scalar only explicitly (a multiprecision value for a `double` bracket, a dimensioned quantity, §6.1). An integer result is accepted when it converts to the scalar or the scalar is constructible from it (`to_scalar` then writes `X(v)`); an integer sign function converges today (52 evaluations), and the rule keeps it accepted. + - **Results.** f returns a real or an integer, or `std::expected` of one. Rejected with a reason, and `std::is_invocable_v` false: `bool`, `void`, `std::optional`, a string, an expression-template result (D16: only expression-template-off types are supported), a proxy or implicitly convertible class, and a type that converts to the scalar only explicitly (a multiprecision value for a `double` bracket, a dimensioned quantity, §6.1). An integer result is accepted when it converts to the scalar or the scalar is constructible from it (`to_scalar`'s `static_cast` then constructs the scalar from it); an integer sign function converges today (52 evaluations), and the rule keeps it accepted. - **A wider floating result** (a `double` f on a `float` bracket) is rounded to the scalar once, without a warning (`to_scalar`). A finite value that rounds to ±inf, or a nonzero value that rounds to 0, fails with `non_finite_value` at that evaluation's cost, on both paths. In constant evaluation the conversion of an out-of-range finite value yields ±inf on GCC 16, Clang 22, MSVC 19.51 and clang-cl 22, so a constexpr solve fails as at run time (measured by the C++ review of the phase-1 core note). The branch is compiled only when the types differ, so no golden row is affected. + - **Revised on 2026-10-06 (§12.21).** `to_scalar` is one `static_cast`. The approved third branch, `X y = v;`, could warn inside the library for a class W that converts to `double` through `long double`, which breaks a consumer's `-Werror` build. For a class type with both an implicit and a better explicit conversion, the cast may pick another conversion than `X y = v;` would, so the CHANGELOG does not claim identical behaviour there. The overflow and underflow check is guarded inline ("both built-in floating types, and different"); the approved guard, `narrows_float_v` over `value_fits_v`, moves to phase 3 with the parameter-width deletion that needs it (below). For a widening pair the check cannot fire (a finite value stays finite and a nonzero value nonzero), so every result is the same as with the approved guard. - **What changes, against today's code** (the counts and warnings were measured by the numerics and C++ reviews of the phase-1 core note; [est] marks an estimate): | Call | Today | After phase 1 | Phase 3 | @@ -882,10 +865,28 @@ template constexpr bool is_fatal(const E&) noexcept; // CPO, }(); ``` - The facades' reason texts are in §6.6. - - **Tests.** Compile-fail `solve_bool_function`, `secant_nonreal_result` and `newton_bool_derivative` (EXPECT "return a real or an integer", `DELETE_REASON`); `brent_braced_wrong_function`'s EXPECT becomes "must be callable with the bracket's scalar type". Concept tests: bool, void, optional, explicit-only and expression-template results, and a bool df, are false for brent, bisection, secant, newton, expand, `solve` and `diff`; int, float, a generic lambda and a `double` f on a `float` bracket are true. `value_fits_v`: `` true; `` and `` false (on cl too); `` true. A user real with an explicit integer constructor compiles with `callback_for_v` and `evaluate`; `cpp_dec_float_50` with expression templates on gives `!callback_for_v`. doctest rows for the overflow case with bisection, brent, `solve` and secant, the underflow case, a `double` f on a `float` bracket and the integer sign function; `consumer_warnings.cpp` adds the `double` f on a `float` bracket. -- **f's parameter type (phase 3; designed only so that phase 3 can build it, nothing of it is built in phase 1).** Phase 3 deletes a bracket wider than f's parameter (the table above) with these traits, sketched here: - - `param_of`: the parameter type of a function, a function pointer, one non-template `operator()` (including the `noexcept` and ref-qualified forms), `std::function` and `std::reference_wrapper` (all checked on GCC 16.1, Clang 22.1.8, MSVC 19.51 and clang-cl 22.1.3 on a prototype by the C++ review of the phase-1 core note). Generic lambdas and overloaded call operators have none. It must also recognise an explicit object parameter (`double operator()(this const S&, float)`, whose `&S::operator()` is `R(*)(S, P)` with S the class), which the prototype missed. - - `has_param_v` and `input_wider_than_param_v`, and the facades' deleted siblings that use them. + - **Tests** (revised on 2026-10-06, §12.21). Compile-fail `solve_bool_function` and `newton_bool_derivative` (EXPECT "return a real or an integer", `DELETE_REASON`). The approved `secant_nonreal_result` is dropped: it reaches the same deleted declaration, the open facade's, as `newton_bool_derivative`, and every reason keeps at least one compile-fail case with `DELETE_REASON`. `brent_braced_wrong_function`'s EXPECT becomes "must be callable with the bracket's scalar type". Concept tests: the result kinds once, on `callback_for_v` (bool, void, optional and explicit-only results false; int, float and a generic lambda true; the expression-template kind under multiprecision only, where `cpp_dec_float_50` with expression templates on gives `!callback_for_v`); then one bad (bool) and one good (int) row for each entry point, brent, bisection, secant, newton, expand, `solve` and `diff`, and newton's bool df; a `double` f on a `float` bracket is true. A user real with an explicit integer constructor compiles with `callback_for_v` and `evaluate`. doctest rows: one overflow row per evaluation path (bisection for `evaluate_sample`, secant for `evaluate`), the underflow row on both, a `double` f on a `float` bracket and the integer sign function; `consumer_warnings.cpp` adds the `double` f on a `float` bracket. The `value_fits_v` static_asserts move to phase 3 with the trait (below). +- **f's parameter type (phase 3; designed only so that phase 3 can build it, nothing of it is built in phase 1).** Phase 3 deletes a bracket wider than f's parameter (the table above). Sketched here, as revised on 2026-10-06 (§12.21): + - **The mechanism phase 3 evaluates first** is two requires-expressions on the unwrapped callable (`nxx::detail::unref`), with X the bracket's scalar. They replace the approved `param_of` family (about 8 specialisations), and they cover an explicit object parameter too: + + ```cpp + has_param = requires(const F& fn) { fn({}); }; // a generic or overloaded call operator cannot satisfy it + wider = has_param && requires(const F& fn, const float& s) { fn({s}); } // {s} narrows into an integer or bool + && !requires(const F& fn, const X& x) { fn({x}); }; // parameter, so those stay out + ``` + - **`value_fits_v` and `narrows_float_v`** move here from phase 1, with their 4 static_asserts (`` true; `` and `` false, on cl too; `` true), because only this deletion needs them: + + ```cpp + // From converts to To without narrowing: built-in floating types only (otherwise true), by list-initialisation. + template inline constexpr bool value_fits_v = [] { + if constexpr (std::is_floating_point_v && std::is_floating_point_v) return requires(From v) { To{v}; }; + else return true; + }(); + template inline constexpr bool narrows_float_v = + std::is_floating_point_v && std::is_floating_point_v && !value_fits_v; + ``` + - **Open, to decide in phase 3:** an integer parameter on a floating bracket (x truncates, so f becomes a step function, §7.2) would need its own reason and its own decision. The behaviour of GCC 14 and Clang 19 with narrowing inside a requires-expression is unverified. + - The facades' deleted siblings use these. ### 6.5 Problem types and accepted inputs @@ -925,7 +926,7 @@ better_than(const FEst&, const FEst&) // not a member: found // [phase 1, approved 2026-10-04; not built] ``` -- **The failure estimate's order is a customisation point [phase 1, approved 2026-10-04; not built].** `nxx::better_than(a, b)` is a CPO specified like `nxx::is_fatal` (§6.4): it calls the `better_than(const Est&, const Est&)` that ADL finds next to the estimate type, and is deleted with a reason when there is none. `iterative_solver_for` requires it for the failure estimate type, so a solver whose estimate has no order makes `std::is_invocable_v` false through the facades' protocol reason. Today the driver calls an ADL `better_than` if there is one and falls back to `merit_of(e) < merit_of(best)` (`detail::better`, `core/iterate.hpp`); the fallback is removed. roots' rule becomes a hidden friend of `root_estimate`, so it is found by ADL only and `nxx::roots::better_than`, a namespace-scope function today, disappears (an alpha spelling: CHANGELOG only). A namespace-scope function would be ambiguous with the object `nxx::better_than` in a scope with `using namespace nxx;` and `using namespace nxx::roots;`; the hidden friend was checked under both on GCC 16, Clang 22, MSVC 19.51 and clang-cl 22.1.3 on a prototype (C++ review of the phase-1 core note). Each family's rule is in §6.7. +- **The failure estimate's order is a customisation point [phase 1, approved 2026-10-04; not built].** `nxx::better_than(a, b)` is a CPO specified like `nxx::is_fatal` (§6.4): it calls the `better_than(const Est&, const Est&)` that ADL finds next to the estimate type, and is not invocable when there is none: `std::is_invocable_v` is false, and the compiler's message names `found_v` (Clang: 10 lines, measured on a probe by the simplicity review). It has no deleted sibling (revised on 2026-10-06, §12.21; approved on 2026-10-04 with one): only a direct call on a user estimate type without an order reaches that case, the facades and combinators never do, and the protocol reasons below and the combinators' results texts (§6.10) still name the missing `better_than`. `iterative_solver_for` requires it for the failure estimate type, so a solver whose estimate has no order makes `std::is_invocable_v` false through the facades' protocol reason. Today the driver calls an ADL `better_than` if there is one and falls back to `merit_of(e) < merit_of(best)` (`detail::better`, `core/iterate.hpp`); the fallback is removed. roots' rule becomes a hidden friend of `root_estimate`, so it is found by ADL only and `nxx::roots::better_than`, a namespace-scope function today, disappears (an alpha spelling: CHANGELOG only). A namespace-scope function would be ambiguous with the object `nxx::better_than` in a scope with `using namespace nxx;` and `using namespace nxx::roots;`; the hidden friend was checked under both on GCC 16, Clang 22, MSVC 19.51 and clang-cl 22.1.3 on a prototype (C++ review of the phase-1 core note). Each family's rule is in §6.7. ```cpp namespace nxx { @@ -936,10 +937,7 @@ better_than(const FEst&, const FEst&) // not a member: found struct better_than_fn { template requires found_v constexpr bool operator()(const E& a, const E& b) const noexcept(noexcept(better_than(a, b))) - { return static_cast(better_than(a, b)); } - template requires(!found_v) - bool operator()(const E&, const E&) const NXX_DELETE("nxx::better_than: define better_than(const Est&, const Est&) " - "next to this estimate type, found by ADL: a strict weak order that picks the best estimate a failure carries"); + { return static_cast(better_than(a, b)); } // no deleted sibling: without found_v, not invocable }; } inline constexpr detail::better_cpo::better_than_fn better_than{}; @@ -954,14 +952,14 @@ better_than(const FEst&, const FEst&) // not a member: found - A `better_than` in another namespace, or a member function, counts as missing. A `<=` order trips the `NXX_EXPECTS` in assert builds. - The five protocol reasons of the facades (the "this solver does not implement the solver protocol (DESIGN 6.6)…" texts below) gain ", and better_than(const Est&, const Est&) for its estimate type". - - Tests: a mock estimate without `better_than` gives `!iterative_solver_for` (a concept test); compile-fail `solver_without_better_than` (EXPECT "better_than\\(const Est&, const Est&\\) for its estimate type", `DELETE_REASON`); the tests that call `roots::better_than` call `nxx::better_than`. + - Tests: a mock estimate without `better_than` gives `!iterative_solver_for` and makes `nxx::better_than` not invocable (concept tests); compile-fail `solver_without_better_than` (EXPECT "better_than\\(const Est&, const Est&\\) for its estimate type", `DELETE_REASON`); the tests that call `roots::better_than` call `nxx::better_than`. - **Configuration is one aggregate, builders are generic.** Each solver holds `options` plus algorithm-specific parameters, and declares `template using rebind = …`. The facade provides `with_stop(c)`, `with_budget(max_iterations)`, `with_derivative(d)`, `with_projection(p)` and `with_observer(o)` by rebinding that aggregate with deducing `this`. The alternative, hand-written builders per solver with a private all-members constructor and a friend declaration, was prototyped and is boilerplate **[prototyped]**. - Constructors take nothing, a criterion (`bisection{nxx::width_tol{1e-4}}`), or algorithm-specific parameters (`itp{itp_params{…}}`), with CTAD guides. A bare number is deleted with "a tolerance is a criterion, not a number: write brent{nxx::width_tol{1e-10}}", each solver naming its own criterion. The deletion leaves out a number that the solver's own criterion type converts from, so a user criterion with a converting constructor keeps `brent{tol}`. A deduction guide sends a bare number to `brent<>`, whose deletion then fires: Clang 19.1 deduced `brent` from the implicit guide of `brent(Tol)` despite its constraint, and `brent` has no constructor for a double. Brent's `width_tolerance_v` tests one property at a time (an `if constexpr` lambda, as `criterion_for_v` does): with `&&` in the initializer, `W::applies_to` was formed for every `W`, and `brent{1e-10}` was a hard error inside `brent.hpp`. - - A validated tolerance (`tolerance`) or one of its parts (`abs_tolerance`, `rel_tolerance`) passed to a constructor is not a criterion either: brent, bisection, secant and newton delete both with reasons that name their own criterion (§7.2) **[phase 1, approved 2026-10-04; not built: today a CTAD failure with no reason]**. + - A validated tolerance (`tolerance`) or one of its parts (`abs_tolerance`, `rel_tolerance`) passed to a constructor is not a criterion either: brent, bisection, secant and newton widen their bare-number deletion to them, and its text gains the remedy, naming each solver's own criterion, with no new deletion (§7.2; revised on 2026-10-06, §12.21) **[phase 1, approved 2026-10-04; not built: today a CTAD failure with no reason]**. - There are no positional budgets, which is what makes configuration order-independent. - `with_stop` is constrained on `criterion_for` and has a reasoned deleted sibling. A Tier-A test checks `S{}` and `S{criterion}` for every solver **[sketch]**. - - **`with_stop` with a validated tolerance [phase 1, approved 2026-10-04; not built].** Today `s.with_stop(*tol)` reaches the catch-all deletion and gets the false reason "this criterion does not apply to this solver …" for something that is not a criterion at all (measured by the API-ergonomics review of the phase-1 core note). A separate deleted sibling for `tolerance`, `abs_tolerance` and `rel_tolerance` gets its own reason, and the catch-all (`facade.hpp`) excludes those types, so that cl does not report the two deletions as ambiguous (C2668). The review proposed the text "a validated tolerance is not a criterion: with_stop(nxx::f_tol{*tol}) or a width_tol/x_tol built from it". Whoever builds it checks that each remedy the text names compiles for each solver and each of the three types: brent's `with_stop` rejects a width criterion (§6.8), and `f_tol` takes only a `tolerance`, so the parts may need their own sibling and text, as in the constructors (§7.2). + - **`with_stop` given a non-criterion [phase 1, approved 2026-10-04; not built]** (revised on 2026-10-06, §12.21). Today `s.with_stop(*tol)` reaches the catch-all deletion and gets the false reason "this criterion does not apply to this solver …" for something that is not a criterion at all (measured by the API-ergonomics review of the phase-1 core note), and so does `s.with_stop(1e-10)`, a mistake of medium likelihood for a caller used to 1.x. One deleted sibling, `requires(!is_criterion_v)`, takes everything that is not a criterion: a number, a `tolerance`, an `abs_tolerance` or a `rel_tolerance`. The catch-all (`facade.hpp`) is narrowed to `is_criterion_v && !criterion_for_v`. The two constraints are disjoint, so cl reports no ambiguity (C2668), and `facade.hpp` needs no tolerance trait. The text names the tests in x first, and says what each one bounds (§9.3): "with_stop takes a criterion, not a number or a validated tolerance: wrap it in the test you mean: width_tol{*tol} (bisection; brent takes its width in its constructor) bounds the error in x; x_tol{*tol} (open methods) bounds only the last step in x; f_tol{*tol} bounds only |f(x)|; a part (abs_tolerance, rel_tolerance) is not a criterion either". The approved draft named only `f_tol`, and the numerics and caller lenses of the simplicity review rejected it: it steers a caller toward a residual test, which on a flat f turns an honest budget failure into a criterion success with |x − 1| = 4.5e-4 (`brent{}.with_stop(f_tol{*tol})` with `*tol` = 1e-10 on (x − 1)³ over [0, 3], where `brent{width_tol{*tol}}` fails with `budget_exhausted`). The review's draft said that `x_tol` bounds the error in x too; it bounds only |x_k − x_{k−1}|, and on a multiple root an `x_tol` success leaves a larger error: 1.3 to 6.0 times tol on (x − 1)³ and (x − 1)⁵ (newton and secant from x0 = 2 with a budget of 1000, 501 tolerances log-spaced from 1e-3 to 1e-8; measured on today's solvers with GCC 16.1); for Newton on an m-fold root the ratio lies between (m − 1)²/m and m − 1. Whoever builds it checks that each remedy the text names compiles on the solvers it names: brent's `with_stop` rejects a width criterion (§6.8), and `f_tol` takes only a `tolerance`. Tests: one compile-fail case, `bisection_with_stop_tolerance` (`bisection{}.with_stop(*tol)`, EXPECT "with_stop takes a criterion, not a number", `DELETE_REASON`), and concept tests for a number, a tolerance and a part on each facade. - `with_derivative` and `with_projection` exist only where they mean something (`uses_derivative`, `projects`), and are deleted with a reason elsewhere: "this solver does not use a derivative (newton does)"; "projection applies to open methods (secant, newton): a bracketing method keeps every iterate inside its bracket" **[spike]**. - **Family facades carry the reasons.** Solvers derive from `bracketing_facade`, `open_facade`, `search_facade` or `system_facade`, all deducing-`this` bases with no CRTP **[prototyped]**. Each facade constrains `operator()` on `accepts_v` and "F invocable on the scalar", and `.on()` on `accepts_v` alone (F is not known yet), with a reasoned deletion for every rejected input and for a function that cannot take the input's scalar type. The curried solver (`bound`) is constrained on the solver being invocable with F and its input, without a reasoned deletion, so a wrong function there gets the compiler's generic error (on Clang 22 10 lines with no reason; on GCC 16 the facade's reason at line 33 of 41, measured by the API-ergonomics review of the phase-1 core note). **[phase 1, approved 2026-10-04; not built]** `bound::operator()` gains a deleted sibling, `requires(!std::is_invocable_v)`, with the reason "this solver cannot take this function with its bound input: call solver(f, input) for the reason". `std::is_invocable_v, F, double>` is then `false`, not a hard error (as it is when the check sits inside the body). The reasons: - "bracketing solvers need a bracket: pass {lo, hi}, nxx::bracket::make(a, b), or a search result"; @@ -1057,21 +1055,28 @@ This one loop replaces `fsolve_impl`, `fdfsolve_impl`, `search_impl`, `integrate `detail::better` is per family (through the family's `better_than`, §6.6): - roots: **an estimate with a sign-changing enclosure beats one without; between two enclosures the narrower (nested, hence newer) wins; ties, and two estimates without enclosures, go by the smaller |fx|**. Without the enclosure rule, a bisection failure could carry an older, wider enclosure. The earlier wording ("if both carry enclosures, the narrower wins; otherwise the smaller |fx|") is not transitive: the spike found three failures where each beat the next, so a static `first_of` (which merges right to left) and the run-time chain (left to right) returned different best estimates, and regrouping a static chain changed its answer. With a strict weak order, every fold order selects the same estimate. - - **Today's order is not a strict weak order once NaN and extreme enclosures enter** (`roots::better_than`, `roots/bracket.hpp`): it compares `width()`, which overflows to inf on wide enclosures, and |fx| with `<`, so a NaN |fx| is incomparable with everything. The numerics review of the phase-1 core note measured 16,564 violations of transitivity of incomparability on 160 random estimates (NaN, ±inf and −0 for fx; extreme, ordinary and subnormal enclosures), and 45 of 20,000 four-estimate chains folded to different best estimates left-to-right and right-to-left. - - **The approved order (R3) [phase 1, approved 2026-10-04; not built]**, a hidden friend of `root_estimate`: + - **Today's order is not a strict weak order once a NaN |fx| enters** (`roots::better_than`, `roots/bracket.hpp`): it compares |fx| with `<`, so a NaN |fx| is incomparable with everything. Its `width()` overflows to inf on wide enclosures, which makes them tie, but that alone does not break the order (corrected on 2026-10-06, §12.21; the review named it as a second cause). The numerics review of the phase-1 core note measured 16,564 violations of transitivity of incomparability on 160 random estimates (NaN, ±inf and −0 for fx; extreme, ordinary and subnormal enclosures), and 45 of 20,000 four-estimate chains folded to different best estimates left-to-right and right-to-left. + - **The approved order (R3) [phase 1, approved 2026-10-04; not built]**, a hidden friend of `root_estimate`, as revised on 2026-10-06 (§12.21). It ranks enclosures by `width()`, today's key, and adds the NaN-last tail: ```cpp friend constexpr bool better_than(const root_estimate& a, const root_estimate& b) noexcept { if (a.enclosure.has_value() != b.enclosure.has_value()) return a.enclosure.has_value(); - if (a.enclosure) { const T ha = a.enclosure->half_width(), hb = b.enclosure->half_width(); // hi/2 - lo/2 - if (ha < hb) return true; if (hb < ha) return false; } + if (a.enclosure) { + T wa = a.enclosure->width(), wb = b.enclosure->width(); + if (!math::isfinite(wa) && !math::isfinite(wb)) { // both overflow: the halves are exact at these magnitudes + wa = a.enclosure->hi() / T(2) - a.enclosure->lo() / T(2); + wb = b.enclosure->hi() / T(2) - b.enclosure->lo() / T(2); + } + if (wa < wb) return true; + if (wb < wa) return false; + } const T fa = math::abs(a.fx), fb = math::abs(b.fx); if (math::isnan(fa)) return false; // NaN last return math::isnan(fb) || fa < fb; } ``` - It is a strict weak order by the key (e, h, n, a), compared lexicographically: e = 0 with an enclosure and 1 without; h = the half-width; n = 1 for a NaN |fx|; a = |fx|, or 0 when it is NaN. h is finite because `sign_bracket` gains `half_width()` and the precondition `NXX_EXPECTS(math::isfinite(lo) && math::isfinite(hi))` in its `trust_me` constructor. The smaller half-width hi/2 − lo/2 equals the width order except where the width overflows, and near the subnormal range, where halving rounds and non-nested enclosures may rank against their widths: with d = `std::numeric_limits::denorm_min()` under round-to-nearest, ties-to-even, [d, 3d] has width 2d and half-width 2d, while [2d, 5d] has width 3d and half-width d (measured in double, 2026-10-04). Multiples of `DBL_MIN` do not show it, because their halves are exact; nested enclosures stay monotone. Measured on a prototype by the numerics review: no axiom violation on the same 160 estimates, and the 20,000 chains fold to the same best in both directions. With it and the input-code change of §6.3 patched into a copy of the headers, the golden table and the combinator tests passed bit for bit on GCC 16.1 and Clang 22.1.8 (same review). Tests: NaN against finite in both argument orders, NaN against NaN, [−max, max] against [−max/2, max], [d, 3d] against [2d, 5d] with d = `denorm_min()`, nested subnormal enclosures; a fixed-seed property test of the four axioms including NaN, ±inf, −0 and extreme or subnormal enclosures; `first_of` over three extreme estimates in every grouping. + It is a strict weak order by the key (e, o, k, n, a), compared lexicographically: e = 0 with an enclosure and 1 without; o = 0 for a finite width and 1 for a width that overflows to inf; k = the width when it is finite, and hi/2 − lo/2 when it is not; n = 1 for a NaN |fx|; a = |fx|, or 0 when it is NaN (o = k = 0 without an enclosure). The `sign_bracket` precondition `NXX_EXPECTS(math::isfinite(lo) && math::isfinite(hi))` in its `trust_me` constructor stays, so the width is never NaN, and every construction site keeps lo < hi (`bracket`, `narrowed()`, brent's and `expand`'s estimates), so it lies in (0, +inf] and the halves are finite: each component is a total preorder. The halves are exact where both widths overflow, because both ends are then far from the subnormal range (brent's `half_step` uses the same pattern). Rounded subtraction is monotone, so the width key never inverts: a strictly narrower enclosure never has a larger key (o, k) than a strictly wider one (two such enclosures can tie when rounding gives them the same width, and then |fx| decides). The order stays consistent with `root_estimate::uncertainty`, which is `width()`, and `sign_bracket` gains no public `half_width()`. The half-width key approved on 2026-10-04 could invert two enclosures near the subnormal range: with d = `std::numeric_limits::denorm_min()` under round-to-nearest, ties-to-even, [d, 3d] has width 2d and half-width 2d, while [2d, 5d] has width 3d and half-width d (measured in double, 2026-10-04). That half-width form was measured on a prototype by the numerics review of the phase-1 core note: no axiom violation on the same 160 estimates, and the 20,000 chains fold to the same best in both directions; with it and the input-code change of §6.3 patched into a copy of the headers, the golden table and the combinator tests passed bit for bit on GCC 16.1 and Clang 22.1.8. The width form was measured on a sketch during the simplicity review (GCC 16.1): no strict-weak-order axiom violation on 220 estimates (NaN, ±inf and −0 for fx; extreme, ordinary and subnormal enclosures), no mismatch against the key (e, o, k, n, a) over 48,400 pairs, no strictly wider enclosure ranked first over 29,584 enclosure pairs (widths compared in long double), and 20,000 four-estimate chains that fold to the same best in both directions. The golden table and the combinator tests were not re-run with it. Tests: NaN against finite in both argument orders, NaN against NaN, [−max, max] against [−max/2, max] (both widths overflow, so the halves decide), [0, max] (finite width) before [−max/2, max] (width overflows), and [d, 3d] before [2d, 5d] with d = `denorm_min()` (widths 2d < 3d; the half-width key inverted them); a fixed-seed property test of the four axioms including NaN, ±inf, −0 and extreme or subnormal enclosures, which also checks that `better_than` never prefers the enclosure with the larger exact width unless both have the same (o, k) (on pairs whose exact order is known, such as nested pairs), because the half-width key also satisfies the four axioms; `first_of` over three extreme estimates in every grouping. - **The enclosure rule's premise** is that an enclosure guarantees a sign change, so a root for a continuous f. **A pole failure therefore carries no enclosure [phase 1, approved 2026-10-04; not built]:** `pole_check`'s `sign_change_not_root` failure (§7.2) carries the estimate with x, fx and uncertainty unchanged and `enclosure = std::nullopt`. Today it carries the final enclosure, which has been shown to hold a pole, and in a merge that outranks every estimate without an enclosure: on tan, `first_of` over brent on [1, 2] and secant from 3 with a budget of 2 fails with best x = 1.5707963267948974 and |fx| = 1.21e15, the pole, instead of secant's x = 3.1415807758403682 with |fx| = 1.19e-5 (measured by the numerics review of the phase-1 core note). After the change the chain's best is secant's estimate. **Known limit:** an enclosure around a pole that the solver did not detect (a budget-exhausted failure) still ranks first. - optimisation: fx under the optimisation sense; - systems: the weighted merit. @@ -1243,69 +1248,60 @@ public: | `with_evaluation_budget(evaluation_budget n, chain)` | One evaluation budget for a whole chain. Each call wraps f in a local counting guard; once n evaluations are spent, every later evaluation fails at zero cost with `evaluations_exhausted`, so the remaining alternatives fail instantly and the merged failure carries the best estimate **[sketch]** | | `any_solver` + `first_of(range)` | Opt-in run-time chains over a run-time list of type-erased curried solvers: the same laziness, merge and cost accounting as `first_of`, and the result is itself an `any_solver` **[prototyped]**; the `is_fatal` policy and `first_of_with(policy, range)` as for static chains **[sketch]** | -**Classified call operators [phase 1, approved 2026-10-04; not built].** Today `first_of_t`, `then_t` and `warm_fallback_t` report misuse with a `static_assert` in the body of an unconstrained call operator with a deduced return type (above), so `std::is_invocable_v` cannot answer false for a misuse: deducing the return type instantiates the body, where the `static_assert` fires. The texts also mislead in two cases. "did you forget .on(input)?" fires whenever an alternative cannot take f, also when every alternative is curried and the cause is a Newton without a derivative or a function of the wrong signature; `then`'s stage-2 text blames a bracket when stage 2 is a Newton without a derivative (both measured by the API-ergonomics review of the phase-1 core note). The approved form classifies each call, with one constrained `operator()` for `== ok` and one deleted sibling per other state. The constraints are equality tests on a variable template (`first_of_call_v<…> == first_of_call::ok`), not folds (§5.3). Each class keeps its body. `core/compose.hpp` does not include `facade.hpp` (§5.2), so a bare solver is detected structurally: +**Classified call operators [phase 1, approved 2026-10-04; not built].** Today `first_of_t`, `then_t` and `warm_fallback_t` report misuse with a `static_assert` in the body of an unconstrained call operator with a deduced return type (above), so `std::is_invocable_v` cannot answer false for a misuse: deducing the return type instantiates the body, where the `static_assert` fires. The texts also mislead in two cases. "did you forget .on(input)?" fires whenever an alternative cannot take f, also when every alternative is curried and the cause is a Newton without a derivative or a function of the wrong signature; `then`'s stage-2 text blames a bracket when stage 2 is a Newton without a derivative (both measured by the API-ergonomics review of the phase-1 core note). The approved form classifies each call, with one constrained `operator()` for `== ok` and one deleted sibling per other state. The constraints are equality tests on a variable template (`first_of_call_v<…> == first_of_call::ok`), not folds (§5.3). Each class keeps its body. **Revised on 2026-10-06 (§12.21):** each class has one "cannot take" state, one "results" state and, for `then` and `warm_fallback`, one "stage 2 cannot start" state. That makes 11 states (ok included) instead of the 21 approved on 2026-10-04, and 8 deleted siblings with 8 reason texts instead of 18. The approved structural detection of a bare solver (`is_bare_solver_v`, and `uncurried_v` with its two partial specialisations) and `states_accepts_v` go; the latter was a hard error on GCC 16 for a stage-2 solver that declares `accepts_v` as a plain `static bool`, the pitfall that `facade.hpp` already guards in `states_inputs_v`: ```cpp namespace detail { - // A bare library solver has the protocol's options(); bound, the combinators and any_solver have none. - template inline constexpr bool is_bare_solver_v = requires(const S& s) { s.options(); }; - template inline constexpr bool uncurried_v = sizeof...(A) == 1 && is_bare_solver_v; - template - inline constexpr bool uncurried_v, A...> = uncurried_v || uncurried_v; - template inline constexpr bool uncurried_v, A...> = uncurried_v; - template inline constexpr bool states_accepts_v = [] { - if constexpr (requires { S::template accepts_v; }) return S::template accepts_v; else return false; }(); template inline constexpr bool rebindable_v = // mirrors rebind_failure, whose static_assert goes std::is_same_v || (std::is_same_v && std::is_same_v); - enum class first_of_call : std::uint8_t { ok, not_curried, rejects_function, results_differ, policy_mismatch, no_better_than }; + enum class first_of_call : std::uint8_t { ok, cannot_take, results }; template inline constexpr first_of_call first_of_call_v = [] { - if constexpr (uncurried_v || uncurried_v) return first_of_call::not_curried; - else if constexpr (!callable_v || !callable_v) return first_of_call::rejects_function; + // (0) a nested chain first: when S2 is a first_of_t (first_of(a, b, c) is first_of(a, first_of(b, c))) + // and its own first_of_call_v is not ok, that state, so the caller reads the inner pair's reason; + // read through a trait specialised on first_of_t whose primary template gives ok (likewise then_t), so a + // non-chain S2 names no member + if constexpr (!callable_v || !callable_v) return first_of_call::cannot_take; else { using R1 = std::remove_cvref_t>; using R2 = std::remove_cvref_t>; - if constexpr (!is_result_v || !std::is_same_v) return first_of_call::results_differ; // guard first - else if constexpr (!std::is_invocable_r_v) return first_of_call::policy_mismatch; - else if constexpr (!has_better_than_v) return first_of_call::no_better_than; - else return first_of_call::ok; + if constexpr (!is_result_v || !std::is_same_v) return first_of_call::results; // guard first + else if constexpr (std::is_invocable_r_v + && has_better_than_v) return first_of_call::ok; + else return first_of_call::results; // the policy, or better_than for the estimate type } }(); - enum class then_call : std::uint8_t { ok, not_curried, rejects_function, not_a_result, stage2_rejects_input, stage2_not_ready, failures_differ }; - // then_call_v: the same order. Stage 1's result is checked with is_result_v before value_type is read. - // With V1 = R1::value_type: !callable_v gives stage2_not_ready if states_accepts_v, else - // stage2_rejects_input. R2 is checked with is_result_v. Then rebindable_v. - enum class fallback_call : std::uint8_t { ok, not_curried, rejects_function, not_a_result, stage2_rejects_input, stage2_not_ready, results_differ, no_better_than }; - // The same, with V = R1::error_type::estimate_type (stage 2 starts from the best estimate); after results_differ, - // has_better_than_v, because a failure of both stages merges their best estimates (detail::merge). + enum class then_call : std::uint8_t { ok, cannot_take, stage2_cannot_start, results }; + // then_call_v: (0) S1 a then_t (then(s1, s2, s3) is then(then(s1, s2), s3)) whose own then_call_v is not + // ok gives that state (through the trait, as for first_of); !callable_v gives cannot_take; R1 not a result + // (is_result_v, before value_type is read) gives results; with V1 = R1::value_type, !callable_v gives + // stage2_cannot_start; R2 not a result (is_result_v, checked in its own if constexpr before R2::error_type is + // read), then !rebindable_v, gives results. + enum class fallback_call : std::uint8_t { ok, cannot_take, stage2_cannot_start, results }; + // The same, with V = R1::error_type::estimate_type (stage 2 starts from the best estimate). Its result checks are + // is_result_v, R2 the same type as R1, and has_better_than_v, because a failure of both stages + // merges their best estimates (detail::merge). } ``` -The result is checked with `is_result_v` (§6.3) before `::error_type` or `::value_type` is read. Without that guard a `first_of` over curried callables that return `double`, or `std::expected`, makes `std::is_invocable_v` itself a hard error on GCC 16, Clang 22, MSVC 19.51 and clang-cl 22.1.3 (measured on a prototype by the C++ review of the phase-1 core note). The reasons, with the remedy in the first fragment: +The result is checked with `is_result_v` (§6.3) before `::error_type` or `::value_type` is read, in an `if constexpr` of its own whenever the next test reads a member: `||` in one condition forms both operands, so `!is_result_v || !rebindable_v` makes `then` over a stage 2 that returns `double` a hard error on GCC 16.1 and Clang 22.1.8 (measured on a stub by the C++ review of the 2026-10-06 revision; the two-step form compiles there on GCC, Clang, cl and clang-cl). Without that guard a `first_of` over curried callables that return `double`, or `std::expected`, makes `std::is_invocable_v` itself a hard error on GCC 16, Clang 22, MSVC 19.51 and clang-cl 22.1.3 (measured on a prototype by the C++ review of the phase-1 core note). A nested chain takes the inner chain's state because the approved classifier misled there: when the inner pair of `first_of(a, b, c)` returns different results, it said "an alternative cannot take this function: call it alone", although each alternative works alone (reproduced by the C++ lens of the simplicity review). Canonical call 9 (§6.14) and the quick tour have that shape. Nesting of different kinds needs nothing: calling the inner chain alone gives its reason. The reasons, with the remedy in the first fragment: | Class | State | Reason | |---|---|---| -| `first_of` | not_curried | "nxx::first_of: an alternative is a bare solver called with f only; did you forget .on(input)?" | -| | rejects_function | "nxx::first_of: an alternative cannot take this function: call it alone, alt(f), for its reason (for example newton needs .with_derivative(df), or f must return a real)" | -| | results_differ | today's text: "nxx::first_of: every alternative must return the same std::expected, failure>; adapt the odd one with .transform/.transform_error" | -| | policy_mismatch | "nxx::first_of_with: the policy must be callable with the alternatives' failure and return bool" | -| | no_better_than | "nxx::first_of: the alternatives' estimate type needs better_than(const Est&, const Est&), found by ADL, to pick the best failure payload" | -| `then` | not_curried | "nxx::then: stage 1 is a bare solver called with f only; did you forget .on(input)?" | -| | rejects_function | "nxx::then: stage 1 cannot take this function: call it alone, stage1(f), for its reason" | -| | not_a_result | "nxx::then: each stage must return std::expected, failure>" | -| | stage2_rejects_input | "nxx::then: stage 2 cannot start from stage 1's result (a bracketing solver needs a bracket or a search result: put the bracketing solver first, or a searcher before it)" | -| | stage2_not_ready | "nxx::then: stage 2 accepts stage 1's result but cannot take this function (newton needs .with_derivative(df) or .with_derivative(deriv::numeric{}))" | -| | failures_differ | "nxx::then: the stages report different callback error types; map one with .transform_error so they agree" | -| `warm_fallback` | not_curried, rejects_function, not_a_result | as for `then`, with "nxx::warm_fallback" | -| | stage2_rejects_input | "nxx::warm_fallback: stage 2 must accept stage 1's best estimate (open methods take a root estimate)" | -| | stage2_not_ready | "nxx::warm_fallback: stage 2 accepts the estimate but cannot take this function (newton needs .with_derivative(df))" | -| | results_differ | "nxx::warm_fallback: both stages must return the same result type" | -| | no_better_than | "nxx::warm_fallback: the estimate type needs better_than(const Est&, const Est&), found by ADL, to merge the two stages' best estimates" | - -- **What it replaces.** 18 deleted declarations replace the four `static_assert`s and `rebind_failure`'s assert. The existing compile-fail EXPECT regexes stay. `then`'s text no longer mentions `.from_enclosure()`, which does not exist in the code yet (phase 3). -- **Measured on a prototype** of the `first_of` and `then` classifiers by the C++ review of the phase-1 core note: `std::is_invocable_v` is false and each reason is the first error on GCC 16, Clang 22 and clang-cl; `first_of(static chain, any_solver)` still converts and copies. With this prototype, the result rule (§6.4) and `better_than` (§6.6) patched in together, the syntax-only compile time of test_combinators, test_any_solver and test_solvers changed by 0 to +4 % on GCC and 0 to +3 % on Clang (three runs each). Not prototyped: `warm_fallback`'s classifier, and `uncurried_v` through nested `first_of_t` and `then_t`. Expected diagnostics [est]: GCC about 10–14 lines, Clang about 20–35, because Clang lists every deleted sibling as a candidate (Appendix D). +| `first_of` | cannot_take | "nxx::first_of: an alternative cannot take this function: did you forget .on(input)? If every alternative is curried, call each alone, alt(f), for its reason (newton needs .with_derivative(df); f must return a real)" | +| | results | "nxx::first_of: every alternative must return the same std::expected, failure>; adapt the odd one with .transform/.transform_error; a first_of_with policy takes that result's failure and returns bool; a user estimate type needs better_than(const E&, const E&), found by ADL" (today's text names one Est, although a search result succeeds with a `sign_bracket` and fails with a `root_estimate`, and `is_result_v` accepts it) | +| `then` | cannot_take | "nxx::then: stage 1 cannot take this function: did you forget .on(input)? If it is curried, call it alone, stage1(f), for its reason" | +| | stage2_cannot_start | "nxx::then: stage 2 cannot start from stage 1's result with this function: a bracketing solver needs a bracket or a search result (put it first, or a searcher before it); newton needs .with_derivative(df); call stage 2 alone with stage 1's value for the exact reason" | +| | results | "nxx::then: each stage must return std::expected, failure>; the stages' failures need the same E, and the same UE unless stage 1's is none; map one with .transform_error" (the stages' S differ in `then(expand, brent)`, D29; `rebindable_v` compares only the failures) | +| `warm_fallback` | cannot_take | "nxx::warm_fallback: stage 1 cannot take this function: did you forget .on(input)? If it is curried, call it alone, stage1(f), for its reason" | +| | stage2_cannot_start | "nxx::warm_fallback: stage 2 cannot start from stage 1's best estimate with this function: open methods take a root estimate; newton needs .with_derivative(df); call stage 2 alone with that estimate for the exact reason" | +| | results | "nxx::warm_fallback: both stages must return the same std::expected, failure>; a user estimate type needs better_than(const E&, const E&), found by ADL, to merge the two stages' best estimates" | + +- **What is lost against the approved 21 states.** A curried newton without a derivative, or a bool f, inside a chain no longer gets a reason of its own: the cannot-take text names both causes, and calling the alternative alone gives the exact one. The exotic states (a policy of the wrong type, a missing `better_than`, a stage that returns no Numerixx result) are clauses of the results text, not texts of their own. A state with no sibling of its own was rejected: the compiler would then show the other siblings' reasons ("did you forget .on?") as candidate notes, which misleads. +- **What it replaces.** 8 deleted declarations (18 as approved on 2026-10-04) replace the four `static_assert`s and `rebind_failure`'s assert. The existing compile-fail EXPECT regexes stay. `then`'s text no longer mentions `.from_enclosure()`, which does not exist in the code yet (phase 3). +- **Measured on a prototype** of the approved (2026-10-04) `first_of` and `then` classifiers by the C++ review of the phase-1 core note: `std::is_invocable_v` is false and each reason is the first error on GCC 16, Clang 22 and clang-cl; `first_of(static chain, any_solver)` still converts and copies. With this prototype, the result rule (§6.4) and `better_than` (§6.6) patched in together, the syntax-only compile time of test_combinators, test_any_solver and test_solvers changed by 0 to +4 % on GCC and 0 to +3 % on Clang (three runs each). On a mock of the revised `warm_fallback`, the simplicity review measured about 31 Clang lines for a misuse instead of 55. Not prototyped, to measure when built: the nested-state propagation and the `warm_fallback` classifier. Expected diagnostics [est]: GCC about 10–14 lines, Clang about 20–35, because Clang lists every deleted sibling as a candidate (Appendix D). - **`any_solver` keeps its one reason.** Its deleted constructor still fires only for a solver that is callable with F and returns another result type (the reason in the code below). A chain that F cannot call gets the compiler's plain conversion error, with no false hint, and `std::is_invocable_v` on it is false. A reason for that case was rejected because a deleted constructor that matches every type would make `any_solver` a viable conversion target everywhere (D35). - `budgeted_t` and `with_evaluation_budget` (phase 3) follow the same pattern. -- **Tests.** The compile-fail cases `first_of_without_on`, `first_of_mismatch` and `then_open_to_bracket` gain `DELETE_REASON`, with their regexes unchanged. New: `first_of_newton_without_derivative` and `first_of_bool_function` ("cannot take this function"), `first_of_not_a_result` ("must return the same"), `first_of_user_estimate_without_better_than`, `first_of_with_bad_policy`, `then_first_stage_not_curried`, `then_newton_without_derivative` ("accepts stage 1's result but cannot take"), `then_mixed_causes`, `warm_fallback_wrong_stage2`, `warm_fallback_user_estimate_without_better_than`. Concept tests: curried callables returning `double` and `std::expected` give false, not a hard error; a three-alternative `first_of` whose third alternative is not curried is `not_curried`; a `warm_fallback` over two curried callables that return the same Numerixx result, whose estimate type has no `better_than`, gives false (`no_better_than`), not a hard error while its return type is deduced; every canonical chain is invocable; `first_of(static…, *runtime_chain)` still compiles. +- **Tests** (revised on 2026-10-06, §12.21). Each of the 8 siblings keeps at least one compile-fail case with `DELETE_REASON`. The existing cases `first_of_without_on`, `first_of_mismatch` and `then_open_to_bracket` gain `DELETE_REASON`, with their regexes unchanged ("did you forget \\.on\\(input\\)", "must return the same", "stage 2 cannot start from stage 1"), and cover first_of's cannot_take and results and then's stage2_cannot_start. 6 new cases: `then_first_stage_not_curried` (then, cannot_take), `then_mixed_causes` (then, results), `warm_fallback_first_stage_not_curried` (cannot_take), `warm_fallback_wrong_stage2` (stage2_cannot_start), `warm_fallback_mixed_results` (results), and `first_of_nested_mismatch`: `first_of(a, b, c)` whose inner pair returns different results gives the results reason, not cannot_take. Concept tests (`std::is_invocable_v` false, not a hard error): a `first_of_with` policy typed for the wrong failure; a user estimate type without `better_than` in `first_of` and in `warm_fallback` (while the return type is deduced); curried alternatives returning `double` or `std::expected`; `then` over a curried stage 1 that returns `double`, and over a stage 2 that returns `double`; a bool f; a newton without a derivative; a three-alternative chain missing `.on` on its third alternative. Every canonical chain is invocable, and `first_of(static…, *runtime_chain)` still compiles. **Run-time chains (opt-in) [prototyped].** Static chains cannot be assembled from configuration. `any_solver` erases the type of any curried solver (a `bound` solver, a `then_t`, a `first_of_t`, another run-time chain) whose call `const F& → result` matches exactly, for one fixed callable type `F` such as `std::function`. In the library it is ``, the only header that uses `std::function`, included by neither `core.hpp` nor `numerixx.hpp`. The code below is the prototype's `nxx/runtime.hpp`, verbatim as compiled; it passed on all 9 configurations, with results bit-identical to the equivalent static chains: @@ -1669,22 +1665,26 @@ template struct numeric { | `halley` | guess + f′, f″ (+ optional sign_bracket) | 8 (optional) | Add | accepts one callable returning `{f, f′, f″}`; with a bracket it falls back to bisection when a step leaves it (as rtsafe does) | | `steffensen` | guess | 8 (optional) | Fix (low priority) | no Newton first step, no throw; deleted if the corpus shows no benefit | -**Validated tolerances in the constructors [phase 1, approved 2026-10-04; not built].** A `tolerance` (from `make()`, or a configuration struct) is not a criterion. Today `r::brent{*tol}` and `r::bisection{*tol}` fail with a long CTAD error and no reason (104 lines on GCC 16 and 59 on Clang 22 for bisection, measured by the API-ergonomics review of the phase-1 core note). brent gets two deletions, one for the tolerance and one for its parts; the tolerance's remedy compiles at run time, because `width_tol(tolerance)` is constexpr (§6.2), while for the parts it would not (`width_tol` has no constructor from an `abs_tolerance` alone, and the mixed constructor is consteval), so they get their own text: +**Validated tolerances in the constructors [phase 1, approved 2026-10-04; not built]** (revised on 2026-10-06, §12.21). A `tolerance` (from `make()`, or a configuration struct) is not a criterion, and neither is one of its parts. Today `r::brent{*tol}` and `r::bisection{*tol}` fail with a long CTAD error and no reason (104 lines on GCC 16 and 59 on Clang 22 for bisection, measured by the API-ergonomics review of the phase-1 core note). No new deletion: each solver's existing bare-number deletion (§6.6; `brent.hpp:119-121`, `bisection.hpp:63-65`, `secant.hpp:64-66`, `newton.hpp:104-106` today) is widened to validated tolerances and their parts, and its text gains the remedy of §12.20's decision 12 and a clause for a part. The tolerance's remedy compiles at run time, because `width_tol(tolerance)` is constexpr (§6.2); for a part it would not (`width_tol` has no constructor from an `abs_tolerance` alone, and the mixed constructor is consteval), so the text sends a part to `make`: ```cpp // roots/brent.hpp -template requires(nxx::detail::is_tolerance_v> && !std::is_convertible_v) -explicit brent(R) NXX_DELETE("a validated tolerance is not a criterion; wrap it: brent{nxx::width_tol{*tol}}"); -template requires nxx::detail::is_tolerance_part_v> -explicit brent(R) NXX_DELETE("an absolute or relative part alone is not a criterion: build one with " - "nxx::width_tol::make(abs, rel), or width_tol{a, nxx::rel_tolerance{r}} for literals"); -template requires(nxx::detail::is_tolerance_v> || nxx::detail::is_tolerance_part_v>) -brent(R) -> brent<>; // so the deletions report, not CTAD +template + requires((std::is_arithmetic_v || real || nxx::detail::is_tolerance_v || nxx::detail::is_tolerance_part_v) + && !std::is_convertible_v) +explicit brent(R) NXX_DELETE("a tolerance is a criterion, not a number: write brent{nxx::width_tol{1e-10}}; a validated " + "tolerance is not a criterion either; wrap it: brent{nxx::width_tol{*tol}}; a part " + "(abs_tolerance, rel_tolerance) is not one either: build the criterion with nxx::width_tol::make"); +template // the existing guide (brent.hpp:244-246), widened the same way, + requires(std::is_arithmetic_v || real || nxx::detail::is_tolerance_v || nxx::detail::is_tolerance_part_v) +brent(R) -> brent<>; // so the deletion reports, not CTAD ``` -- bisection, secant and newton get the same pair, each naming its own criterion as the bare-number deletion does (§6.6): bisection with the width texts (`bisection{nxx::width_tol{*tol}}`, `nxx::width_tol::make(abs, rel)`), secant and newton with the `x_tol` texts (`secant{nxx::x_tol{*tol}}`, `nxx::x_tol::make(abs, rel)`). -- The `!std::is_convertible_v` clause keeps a user criterion that converts from a tolerance working, as for the bare number (§6.6). -- Tests: compile-fail `brent_validated_tolerance` ("wrap it: brent") and `brent_tolerance_part` ("an absolute or relative part alone"), with `DELETE_REASON`, and the same pair for bisection, secant and newton; static asserts that `brent{width_tol}` builds, `brent{tolerance}`, `brent{abs_tolerance}` do not, and `brent>` is constructible from a `tolerance` (through the converting constructor of `width_tol`). +- bisection, secant and newton widen theirs the same way, with `stop_type` in place of `Tol`, each naming its own criterion: bisection `width_tol` (`bisection{nxx::width_tol{*tol}}`, `nxx::width_tol::make`), secant and newton `x_tol` (`secant{nxx::x_tol{*tol}}`, `nxx::x_tol::make`). They need no guide: their implicit guide, from the defaulted `Opt`, reaches the deletion, as it does for a bare number. +- The `!std::is_convertible_v` clause keeps `brent>{tol}` constructible (through the converting constructor of `width_tol`), and a user criterion that converts from a tolerance working, as for the bare number (§6.6). +- The existing EXPECT regexes ("a tolerance is a criterion, not a number: write brent", and the same for bisection, secant and newton) still match. Callers who write `brent{1e-10}` read the longer text. +- Tests: compile-fail `brent_validated_tolerance` (through brent's explicit guide) and `bisection_validated_tolerance` (through the implicit guide), EXPECT "wrap it: brent" and "wrap it: bisection", with `DELETE_REASON`; static asserts, through concepts, that brent, bisection, secant and newton reject `tolerance`, `abs_tolerance` and `rel_tolerance`, that `brent{width_tol}` builds, and that `brent>` is constructible from a `tolerance`. +- Against the approved form (two new deletions per solver), this saves 8 deleted declarations, 8 reason texts, 1 guide and 6 compile-fail cases; 4 existing texts are extended instead. **Open-method safeguards** (newton, secant; halley and steffensen when added): - **Progress window** in the state (default `progress_window{4}`, configurable, and can be disabled) **[sketch]**: @@ -1696,7 +1696,7 @@ brent(R) -> brent<>; // so the deletions repo - `root_estimate::uncertainty` = |x_k − x_{k−1}|, or inf for an estimate with neither an enclosure nor a step: the best endpoint of a no-sign-change or search failure, or an open method that stops at its start on an exact zero (secant and Newton agree) **[spike]**. `exact_zero` means "f evaluated to 0", not "full accuracy". Newton on the expanded (x−1)³ stops `exact_zero` at an error of 4.7e-6, the ε^(1/3) conditioning limit (measured). Other roots items: -- **Failure order** (`better_than`, a hidden friend of `root_estimate`, §6.6, §6.7): an estimate with an enclosure first, then the smaller half-width, then the smaller |fx|, with a NaN |fx| last; a pole failure's estimate carries no enclosure (above) **[phase 1, approved 2026-10-04; not built: today's `roots::better_than` compares widths, and has no rule for NaN]**. +- **Failure order** (`better_than`, a hidden friend of `root_estimate`, §6.6, §6.7): an estimate with an enclosure first, then the smaller width (when both widths overflow, the smaller hi/2 − lo/2), then the smaller |fx|, with a NaN |fx| last; a pole failure's estimate carries no enclosure (above); by width, not half-width, since the revision of 2026-10-06 (§12.21) **[phase 1, approved 2026-10-04; not built: today's `roots::better_than` compares widths too, and has no rule for NaN]**. - **Default choice (D29):** the phase-3 corpus records mean and worst-case evaluation counts for bisection, illinois, ridders and brent, and fixes the bracketing default. Phase 8 re-runs it with toms748 and itp; the default changes only with a documented behaviour note. TOMS748 usually needs fewer evaluations on smooth f; ITP has the best worst-case bound. - **Drop:** - `fsolve`/`fdfsolve`/`search` and their template-template drivers; @@ -2148,7 +2148,7 @@ Sizes are focused developer-days for one developer (rough). Every phase also has |---|---|---|---|---|---| | 0 | Skeleton | Tag `v1.0.0`/`v1.1.0-legacy` (§10.4); delete the old tree, vcpkg, gcem, Blaze, gbench, `.idea`; new CMake, presets, CI with every §9.4 leg, including `gcc-multiprecision` (standalone Boost.Multiprecision fetched for tests; `cpp_bin_float_50` satisfies `real` without the adapter, D16), so phases 1–8 add MP instantiations as they land | buildable empty library; CI | `cmake --workflow --preset gcc` from a clean clone; consumers job green, including the scalar-only leg with `NUMERIXX_WITH_FXT=OFF` and `NUMERIXX_WITH_LINALG=OFF`; `gcc-multiprecision` leg green | 2.5–3.5 | | S | Spike | §10.2 | spike branch merged | exit criteria 1–11 | 3–4 | -| 1 | Core vocabulary | `config.hpp` macros; `scalar_traits`, `nxx::math` helpers (incl. `midpoint`); refined types (`abs_tolerance`, `evaluation_budget`, `is_refined_v`); `errc`/`fault`/`failure`/`solution`; `evaluate`/`evaluate_sample`; `cost_of`; unwrapping and common cause; `copyable_box`; `numerixx::pipes`. Added on 2026-10-04 (§12.20) **[phase 1, approved 2026-10-04; not built]**: the renames `failure::by` and `fault::evaluations`, `nxx::best` and the reasoned `best`/`best_x` deletions (§6.3); `non_finite_input` for every non-finite input value and the driver's step-code mapping (§6.3, §6.7); role-typed `x_tol`/`width_tol` literals, the `make` set and the deletions for validated tolerances (§6.2, §7.2); the result rule and the narrowing check for callbacks (§6.4); `nxx::better_than` with the R3 order and the pole payload (§6.6, §6.7); classified combinator call operators (§6.10); the `with_stop` and `bound` reasons (§6.6); canonical calls 13–15 (§6.14) | `numerixx::core`, `numerixx::pipes` | illegal-state and compile-fail suites; defaults-achievable `static_assert`s for `float`, `double` and `long double`, and for `cpp_bin_float_50` constexpr arithmetic on `digits` plus a run-time check (changed on 2026-10-04: `cpp_bin_float_50` is not a literal type, §3.5); result sizes recorded | 2.5–3.5 (re-estimated on 2026-10-04 with the added scope: 6.5–9.5 [est], below) | +| 1 | Core vocabulary | `config.hpp` macros; `scalar_traits`, `nxx::math` helpers (incl. `midpoint`); refined types (`abs_tolerance`, `evaluation_budget`, `is_refined_v`); `errc`/`fault`/`failure`/`solution`; `evaluate`/`evaluate_sample`; `cost_of`; unwrapping and common cause; `copyable_box`; `numerixx::pipes`. Added on 2026-10-04 (§12.20) and revised on 2026-10-06 (§12.21) **[phase 1, approved 2026-10-04; not built]**: the renames `failure::by` and `fault::evaluations`, `nxx::best` and the reasoned `best` deletion (§6.3); `non_finite_input` for every non-finite input value and the driver's step-code mapping (§6.3, §6.7); role-typed `x_tol`/`width_tol` literals, the `make` set and the widened deletions for validated tolerances (§6.2, §7.2); the result rule and the overflow and underflow check for callbacks (§6.4); `nxx::better_than` with the R3 order and the pole payload (§6.6, §6.7); classified combinator call operators (§6.10); the `with_stop` and `bound` reasons (§6.6); canonical calls 13–15 (§6.14) | `numerixx::core`, `numerixx::pipes` | illegal-state and compile-fail suites; defaults-achievable `static_assert`s for `float`, `double` and `long double`, and for `cpp_bin_float_50` a run-time check (changed on 2026-10-04, because `cpp_bin_float_50` is not a literal type, §3.5, and on 2026-10-06, §12.21); result sizes recorded | 2.5–3.5 (re-estimated with the added scope on 2026-10-04 to 6.5–9.5 [est], and on 2026-10-06 to 5–7.5 [est], below) | | 2 | deriv | stencils, step specs (optimal, relative, noise, absolute), `diff` + conveniences, `diff_with_error`, `ridders`, `mixed`, `derivative_of`, `numeric` | `numerixx::deriv` (core only) | deriv corpus incl. x ∈ {1e-3, 1e-8} and mixed scales; stencil-order log-log property; error-estimate reliability; the §9.3 derivative scenarios | 3–5 | | 3 | Driver + 1-D roots | criteria with view kinds; driver (`detail::advance`, `finish`); family facades, options, builders, input overloads; `first_of_t`/`then_t`/`warm_fallback_t`/`with_evaluation_budget`; **`any_solver` + `first_of` over a range**; **`steps_view`**; bisection, brent, illinois (Anderson–Björck), ridders, rtsafe, secant, newton; expand, scan, subdivide; `solve` facade; `inverse_of` | `numerixx::roots` | criterion soundness; roots corpus (`float`/`double`/`long double`, MP) incl. poles, extreme brackets, small roots, a root exactly at 0, cycles, and the Alefeld–Potra–Shi problems; counts match instrumented f; default bracketing solver chosen by corpus counts; the §9.3 root scenarios; `steps_view` iterates equal the driver's; run-time chain equals the static chain; canonical calls 1–3, 9–12 | 10–14 | | 4 | optimize | golden, brent_min (intrinsic test), bracket_minimum, `maximizing`/`maximize`, `minimizer_of` | `numerixx::optimize` | optimize corpus incl. `float`; max = min(−f); the §9.3 optimisation scenarios; canonical call 4 | 2.5–4.5 | @@ -2158,15 +2158,15 @@ Sizes are focused developer-days for one developer (rough). Every phase also has | 8 | Optional roots | toms748 (attributed Boost.Math port), itp (`itp_params`, frozen ε_ITP), halley (combined callable, bracket fallback), steffensen (deleted if the corpus shows no benefit); re-run the default-choice corpus | roots additions | agreement with the Boost.Math TOMS748 oracle; ITP within its worst-case bound on the corpus; criterion soundness for each; default-solver decision recorded | 4 | | 9 | Multiprecision, docs, release | multiprecision adapter (`numerixx::multiprecision`: `adapters/multiprecision.hpp`, `adapters/multiprecision_linalg.hpp`) and the MP linalg tests on the existing `gcc-multiprecision` leg; docs rewritten (dev-reorg `docRoots.rst` structure, "no error handling" policy inverted); examples; benchmarks | `v2.0.0` | docs build; examples are smoke tests; MIGRATION.md complete; MP leg green including linalg | 5–7 | -**Phases 1–3 start from the spike's code** (decided on 2026-09-29). The spike built first cuts of phase 1–3 code in the library tree, more than its exit criteria needed (§10.2, Appendix D). That code is kept, and phases 1–3 continue from it. Their scope and acceptance criteria are unchanged, except that phase 1 gained the core changes approved on 2026-10-04 (§12.20), phase 2 gained the deriv items decided with them (not re-estimated), and phase 3 builds the items that design leaves to it (size unchanged [est]); a phase is done only when all of them are met. What the spike built, and what is left: +**Phases 1–3 start from the spike's code** (decided on 2026-09-29). The spike built first cuts of phase 1–3 code in the library tree, more than its exit criteria needed (§10.2, Appendix D). That code is kept, and phases 1–3 continue from it. Their scope and acceptance criteria are unchanged, except that phase 1 gained the core changes approved on 2026-10-04 (§12.20) and revised on 2026-10-06 (§12.21), phase 2 gained the deriv items decided with them (not re-estimated), and phase 3 builds the items that design leaves to it (size unchanged [est]); a phase is done only when all of them are met. What the spike built, and what is left: | Phase | In the spike (a first cut, tested on every preset) | Still to do | Size: planned → left | |---|---|---|---| -| 1 Core vocabulary | the whole scope: `config.hpp` macros, scalar traits, `nxx::math`, refined types, errors and results, `evaluate`/`evaluate_sample`, `cost_of`, unwrapping and common cause, `copyable_box`, `numerixx::pipes` | re-planned on 2026-10-04 (§12.20), in this order, with the size of each step [est]: (1) the renames, `nxx::best` and the `best_x` deletion (§6.3), 0.5; (2) the input codes, the non-finite `diff` x and `checked_step` in the driver and `steps_view` (§6.3, §6.7), 0.5; (3) the role-typed literals, the `make` set, brent's deletions for validated tolerances and the reason texts (§6.2, §6.8, §7.2), 1.25–1.75; (4) `callback_for_v`, `value_fits_v`, `to_scalar` with the overflow and underflow check, the constrained `evaluate`, Newton's df check, the deriv constraints and the facade texts (§6.4, §6.6), 1–1.5; (5) `nxx::better_than`, R3, the `sign_bracket` precondition and the pole payload (§6.6, §6.7, §7.2), 0.5–1; (6) the combinators' classifiers and their 18 deleted siblings, which need (1) and (5) (§6.10), 1–1.5; (7) the three extra reasons: the validated-tolerance deletions on bisection, secant and newton, the `with_stop` sibling and the `bound` sibling (§6.6, §7.2), 0.25–0.5; (8) the acceptance work: A1, the defaults-achievable checks (after 3); A2, the recorded result sizes (last among the code changes); A3 and A4, the documentation of the `-ffp-contract` and `numeric_limits` decisions, written into §5.3 and §6.1 with this design; 0.6–0.85 together; (9) CHANGELOG, MIGRATION rows, the DESIGN status marks, `canonical_calls.cpp`, reviews, and all 12 presets from `--fresh`, 1–1.5; (10) a nightly dispatch for the compiler floors once the narrowing check exists, after asking the user; (11) the tag `v2.0.0-alpha.1` | 2.5–3.5 → 6.5–9.5 [est] (the step sizes sum to 6.6–9.6; the approved figure is rounded to 6.5–9.5, §12.20; 0.5–1 before the 2026-10-04 additions) | +| 1 Core vocabulary | the whole scope: `config.hpp` macros, scalar traits, `nxx::math`, refined types, errors and results, `evaluate`/`evaluate_sample`, `cost_of`, unwrapping and common cause, `copyable_box`, `numerixx::pipes` | re-planned on 2026-10-04 (§12.20) and again on 2026-10-06 (§12.21), in three build PRs after the docs PR that records the revision, with the size of each step [est]. **PR 1, results and order:** (1) the renames and `nxx::best` (§6.3), 0.4–0.5; (2) the input codes, the non-finite `diff` x and `checked_step` in the driver and `steps_view` (§6.3, §6.7), 0.5; (3) `nxx::better_than`, R3 by width, the `sign_bracket` precondition and the pole payload (§6.6, §6.7, §7.2), 0.35–0.85. **PR 2, tolerances:** (4) the role-typed literals, the `make` set, the widened solver deletions and the reason texts (§6.2, §6.8, §7.2), 1–1.5; (5) the `with_stop` and `bound` siblings (§6.6), 0.1–0.25; (6) A1, the defaults-achievable checks (§3.5), 0.15–0.25. **PR 3, callbacks and combinators:** (7) `callback_for_v`, `to_scalar` with the overflow and underflow check, the constrained `evaluate`, Newton's df check, the deriv constraints and the facade texts (§6.4, §6.6), 0.7–1.2; (8) the combinators' classifiers and their 8 deleted siblings, which need (1) and (3) (§6.10), 0.6–1; (9) A2, the recorded result sizes (last among the code changes), 0.1–0.15. **In each PR:** (10) CHANGELOG, MIGRATION rows, the DESIGN status marks, `canonical_calls.cpp`, reviews and all 12 presets from `--fresh`, 1–1.5 in all. Then (11) the nightly floor dispatch once (7) exists, after asking the user, and (12) the tag `v2.0.0-alpha.1`. A3 and A4, the documentation of the `-ffp-contract` and `numeric_limits` decisions, are already written into §5.3 and §6.1 | 2.5–3.5 → 5–7.5 [est] (the step sizes sum to 4.9–7.7, stated as 5–7.5, §12.21; 6.5–9.5 on 2026-10-04, when the steps summed to 6.6–9.6; 0.5–1 before the 2026-10-04 additions) | | 2 deriv | the stencils `central_1_2`, `central_1_4`, `central_2_2`, `central_2_4`, `forward_1_1` and `backward_1_1`; the steps `optimal`, `relative` and `absolute`; `diff`, `central`, `derivative_of`, `numeric` | `noise` steps, `diff_with_error`, `ridders`, mixed partials, `second_derivative_of`, the remaining stencils; the deriv corpus (x ∈ {1e-3, 1e-8}, mixed scales), the stencil-order property, error-estimate reliability and the §9.3 derivative scenarios. The default step needs that corpus: relative steps are scale-invariant for power laws (the derivative of 1/x has a relative error of about 6e-11 from x = 1e-8 to 1e8), but not for exp at large x (8.7e-7 at x = 300 with `central_1_2`, 2.5e-4 with `central_1_4`; measured). Added by the phase-1 design of 2026-10-04 (§7.1, §12.20), not re-estimated: `relative{factor, typical}` names its second role and deletes the bare pair with a reason; deriv's reasons for the result rule; `ridders`' failure order, NaN last | 3–5 → 2–3.5 | -| 3 Driver + 1-D roots | criteria with view kinds; the driver; facades, options, builders and input overloads; `first_of`, `then`, `warm_fallback`; `any_solver` and `first_of` over a range; `steps_view`; bisection, brent, secant, newton, expand; `solve(f, bracket)`. Tested: criterion soundness, poles, extreme brackets, a root at 0, counts equal to instrumented calls, `steps_view` equal to the driver, run-time chains equal to static chains, canonical calls 1–3, 9 and 10 | `with_evaluation_budget`; illinois, ridders, rtsafe; scan, subdivide; `expand` from a guess, its NaN backtrack and crossing 0; `solve(f, x0)`, `solve(f, df, x0)`; `inverse_of`; the open-method safeguards (without the progress window, Newton on x³ − 2x + 2 from 0 runs its whole budget, 30 iterations and 61 evaluations, before failing); the representation-space bisection midpoint (without it, a root at 1e-200 with a relative tolerance exhausts bisection's 200-step budget); `floored_width`'s `scale`, `step_tol`'s `typical`, secant's `x1`; `custom` criteria and `fdf`; the roots corpus with the Alefeld–Potra–Shi problems, and the default-solver choice; canonical calls 11 and 12. Accommodated by the phase-1 design of 2026-10-04 (§12.20), to build here, size unchanged [est]: the deletion of a bracket wider than f's parameter, with `param_of` (including explicit object parameters), `has_param_v` and `input_wider_than_param_v` (§6.4); `budgeted_t` and `with_evaluation_budget` in the classified form (§6.10); a reason for an unchecked `make()` result passed to a solver, as a candidate (§6.2) | 10–14 → 6–9 | +| 3 Driver + 1-D roots | criteria with view kinds; the driver; facades, options, builders and input overloads; `first_of`, `then`, `warm_fallback`; `any_solver` and `first_of` over a range; `steps_view`; bisection, brent, secant, newton, expand; `solve(f, bracket)`. Tested: criterion soundness, poles, extreme brackets, a root at 0, counts equal to instrumented calls, `steps_view` equal to the driver, run-time chains equal to static chains, canonical calls 1–3, 9 and 10 | `with_evaluation_budget`; illinois, ridders, rtsafe; scan, subdivide; `expand` from a guess, its NaN backtrack and crossing 0; `solve(f, x0)`, `solve(f, df, x0)`; `inverse_of`; the open-method safeguards (without the progress window, Newton on x³ − 2x + 2 from 0 runs its whole budget, 30 iterations and 61 evaluations, before failing); the representation-space bisection midpoint (without it, a root at 1e-200 with a relative tolerance exhausts bisection's 200-step budget); `floored_width`'s `scale`, `step_tol`'s `typical`, secant's `x1`; `custom` criteria and `fdf`; the roots corpus with the Alefeld–Potra–Shi problems, and the default-solver choice; canonical calls 11 and 12. Accommodated by the phase-1 design of 2026-10-04 (§12.20), to build here, size unchanged [est]: the deletion of a bracket wider than f's parameter, with the two requires-expressions sketched in §6.4 (in place of `param_of`, revised on 2026-10-06), and `value_fits_v` and `narrows_float_v` with their static_asserts, moved here from phase 1 on 2026-10-06 (§12.21; not re-estimated); `budgeted_t` and `with_evaluation_budget` in the classified form (§6.10); a reason for an unchecked `make()` result passed to a solver, as a candidate (§6.2) | 10–14 → 6–9 | -This moves work into the spike rather than saving it: the spike did much more than its 3–4 days. Left for phases 1–9: **46–69.5 developer-days [est], or 42–65.5 without phase 8**, after the phase-1 re-estimate of 2026-10-04 (before it: 40–61, or 36–57; planned: 47–70, or 43–66). The planned sizes and the totals below are the 2026-09-28 baseline; they leave out the work added to phase 1 on 2026-10-04. +This moves work into the spike rather than saving it: the spike did much more than its 3–4 days. Left for phases 1–9: **44.5–67.5 developer-days [est], or 40.5–63.5 without phase 8**, after the phase-1 re-estimate of 2026-10-06 (§12.21; 46–69.5, or 42–65.5, after that of 2026-10-04; before it: 40–61, or 36–57; planned: 47–70, or 43–66). The planned sizes and the totals below are the 2026-09-28 baseline; they leave out the work added to phase 1 on 2026-10-04. **Totals, and how they follow from the baseline.** The baseline is 57.5–82.5 developer-days: the estimate for an earlier scope that also required zero heap allocation and AD scalars, both now out of scope (§3.5, §3.6), taken without its project-specific migration work. Each change below is a single number applied to both ends of the range. @@ -2323,28 +2323,47 @@ Work also moves between phases (zero net): toms748 (1.5) and itp (1) to phase 8; 1. GCC's floating-point contraction: `-ffp-contract=off` is documented for consumers and not added to the GCC interface flags (§5.3, §6.1). 2. The limits stay in `std::numeric_limits`; this does not rule out a raw-value trait after v2.0 (§6.1, D14). 3. The bare two-number literal of `x_tol` and `width_tol` is deleted with a reason, and `rel_tolerance` names the relative part (§6.2, §6.8). The same rule binds the planned quadrature tolerances (§7.6) and `deriv::relative` (choice 8). - 4. `make(T, T)` is dropped; `make(abs_tolerance, rel_tolerance)` is kept and `make(abs)` is added (§6.2). - 5. A bracket wider than f's parameter is rejected (built in phase 3, decision 6); integer results are accepted (§6.4). - 6. The result checks and the narrowing check are built in phase 1; the facades' parameter-width deletions in phase 3 (§6.4, §10.3). - 7. The nightly floor job is dispatched once the narrowing check exists, after asking the user (§10.3). - 8. The combinators get constrained call operators with reasoned deleted siblings instead of `static_assert`s, accepting that GCC 14 and cl show only the deleted declaration (§3.6, §6.10). - 9. `nxx::better_than` is the documented customisation point for the failure payload's order; the `merit_of` fallback is dropped, and the R3 order (half-widths, NaN last) is folded in (§6.6, §6.7, §7.2). - 10. `failure::where` becomes `by`, `fault::evals` becomes `evaluations`, and `nxx::best(r)` is added (§6.3, D7). + 4. `make(T, T)` is dropped; `make(abs_tolerance, rel_tolerance)` is kept and `make(abs)` is added (§6.2). (amended in §12.21) + 5. A bracket wider than f's parameter is rejected (built in phase 3, decision 6); integer results are accepted (§6.4). (amended in §12.21) + 6. The result checks and the narrowing check are built in phase 1; the facades' parameter-width deletions in phase 3 (§6.4, §10.3). (amended in §12.21) + 7. The nightly floor job is dispatched once the narrowing check exists, after asking the user (§10.3). (amended in §12.21) + 8. The combinators get constrained call operators with reasoned deleted siblings instead of `static_assert`s, accepting that GCC 14 and cl show only the deleted declaration (§3.6, §6.10). (amended in §12.21) + 9. `nxx::better_than` is the documented customisation point for the failure payload's order; the `merit_of` fallback is dropped, and the R3 order (half-widths, NaN last) is folded in (§6.6, §6.7, §7.2). (amended in §12.21) + 10. `failure::where` becomes `by`, `fault::evals` becomes `evaluations`, and `nxx::best(r)` is added (§6.3, D7). (amended in §12.21) 11. `non_finite_input` for every non-finite input value; `invalid_input` for equal ends and overflowing stencils (§6.3, §6.5, §7.1). - 12. `brent{*tol}` is rejected with the reason "a validated tolerance is not a criterion; wrap it: brent{nxx::width_tol{*tol}}" (§7.2). - 13. Phase 1 is re-estimated at 6.5–9.5 developer-days [est] and ends with the tag `v2.0.0-alpha.1` (§10.3). + 12. `brent{*tol}` is rejected with the reason "a validated tolerance is not a criterion; wrap it: brent{nxx::width_tol{*tol}}" (§7.2). (amended in §12.21) + 13. Phase 1 is re-estimated at 6.5–9.5 developer-days [est] and ends with the tag `v2.0.0-alpha.1` (§10.3). (amended in §12.21) - **Choices 1–8** (the note's open implementation choices, each decided as recommended): 1. The driver maps `invalid_input` and `non_finite_input` from any step to `non_finite_value`, in `nxx::iterate` and `steps_view` (§6.3, §6.7). - 2. The purely relative spelling is `width_tol{0.0, nxx::rel_tolerance{r}}`; `width_tol{rel}` is deleted with a reason (§6.2). + 2. The purely relative spelling is `width_tol{0.0, nxx::rel_tolerance{r}}`; `width_tol{rel}` is deleted with a reason (§6.2). (amended in §12.21) 3. A finite wider value that rounds to 0 fails with `non_finite_value`, as one that rounds to ±inf does (§6.4). 4. `sign_change_not_root` carries its estimate without the enclosure, so `first_of` does not rank a pole as its best estimate (§6.7, §7.2). - 5. Three extra reasons: the validated-tolerance deletions on bisection, secant and newton (§7.2); a `with_stop` sibling for validated tolerances, with the catch-all excluding them (§6.6); a `bound::operator()` sibling (§6.6). - 6. A1's acceptance wording: `static_assert`s for `float`, `double` and `long double`; for `cpp_bin_float_50`, constexpr arithmetic on `digits` plus a run-time check (§3.5, §10.3). + 5. Three extra reasons: the validated-tolerance deletions on bisection, secant and newton (§7.2); a `with_stop` sibling for validated tolerances, with the catch-all excluding them (§6.6); a `bound::operator()` sibling (§6.6). (amended in §12.21) + 6. A1's acceptance wording: `static_assert`s for `float`, `double` and `long double`; for `cpp_bin_float_50`, constexpr arithmetic on `digits` plus a run-time check (§3.5, §10.3). (amended in §12.21) 7. Three new canonical calls, 13–15 (§6.14). 8. `deriv::relative{factor, typical}` is bound by decision 3's rule, and is built in phase 2 (§7.1, §10.3). - **Not taken:** `make(rel_tolerance)`; a `make` over two `std::expected` parts; a reason for an unchecked `make()` result passed to a solver (a phase-3 candidate); a reason on `any_solver` for a chain that F cannot call (§6.2, §6.10). - **Known gap after phase 1:** cl warns (C4244) for a `float` f on a `double` bracket until phase 3 deletes that call (Appendix D). +**Decided on 2026-10-06: the phase-1 simplicity review.** The user accepted every recommendation of a simplicity review of the design approved in §12.20: six `simplicity-reviewer` passes (five on single items, one across the whole design), with every proposal then checked by four verification lenses (numerics, C++, caller and phase scope), which corrected eight of them; no change weakens a rule-5 guarantee (§9.3). None of it is built yet: the sections cited keep the mark **[phase 1, approved 2026-10-04; not built]** and say what changed on 2026-10-06. + +21. **Phase-1 simplicity revisions.** + 1. **The combinators' states** (amends decision 8's state list): per class, one "cannot take" state, one "results" state and, for `then` and `warm_fallback`, one "stage 2 cannot start" state. That makes 11 states instead of 21, and 8 deleted siblings and 8 reason texts instead of 18. A nested chain takes the inner chain's state. `is_bare_solver_v`, `uncurried_v` and `states_accepts_v` go; `rebindable_v`, the three classifiers, `is_result_v` and `has_better_than_v` stay (§3.6, §6.10). + 2. **Validated tolerances in solver constructors** (amends decision 12 and choice 5): no new deletion. Each solver's bare-number deletion is widened to `tolerance`, `abs_tolerance` and `rel_tolerance`, and brent's guide with it; the text gains decision 12's remedy and a clause for a part. That is 8 deleted declarations, 8 texts, 1 guide and 6 compile-fail cases fewer (§6.6, §7.2). + 3. **`with_stop` given a non-criterion** (amends choice 5): one deleted sibling, `requires(!is_criterion_v)`, for numbers, validated tolerances and parts, with the catch-all narrowed to criteria. Its text names the tests in x (`width_tol`, `x_tol`) before `f_tol`, and says that only `width_tol` bounds the error in x. It also fixes today's false reason for `with_stop(1e-10)` (§6.6). + 4. **`make(abs, rel_tolerance)` is accepted** (amends decision 4's set) as the run-time mirror of the literal `width_tol{a, nxx::rel_tolerance{r}}`, instead of being deleted with "validate the absolute part too". A mixed run-time tolerance takes two checks instead of three; `make(T, T)` stays deleted (§6.2). + 5. **A part alone in a literal** (amends choice 2): the deletion of `width_tol{rel}` and its guide are keyed on `is_tolerance_part_v`, so `width_tol{nxx::abs_tolerance{a}}` gets the reason too, and `is_any_rel_v` goes (§6.2). + 6. **No `best_x` deletion** (amends the scope that came with decision 10): `best_x` keeps today's constraint, and its doc comment names the remedy. `nxx::best` keeps its deletion and deduces the result type directly, without `same_estimate_v` (§6.3, §10.3). + 7. **No deleted sibling on `nxx::better_than`** (amends decision 9): without an ADL `better_than` it is not invocable, and the compiler names `found_v` (§6.6). + 8. **R3 by width** (amends decision 9): enclosures rank by `width()`, by hi/2 − lo/2 only when both widths overflow, and a NaN |fx| ranks last. `sign_bracket` gains no `half_width()`, and the subnormal caveat goes. Of its two test rows, the nested subnormal row goes and [d, 3d] against [2d, 5d] stays with the opposite expectation, so that a return to the half-width key fails a test (§6.7, §7.2). + 9. **`value_fits_v` and `narrows_float_v` move to phase 3** (amends decisions 6 and 7). The run-time overflow and underflow check stays, guarded inline; `to_scalar` is one `static_cast`. The nightly floor job is dispatched once the callback checks exist (PR 3), after asking the user. The callback test plan is slimmer (§6.4, §10.3). + 10. **The phase-3 sketch** (amends the sketch for decisions 5 and 6): two requires-expressions on the unwrapped callable replace the `param_of` family; an integer parameter on a floating bracket is left open for phase 3 (§6.4, §10.3). + 11. **A1 for `cpp_bin_float_50`** (amends choice 6): a run-time check that calls the library's own thresholds, in place of constexpr integer arithmetic on `digits` (§3.5, §10.3). + 12. **The estimate and the build order** (amends decision 13's estimate and the order of §10.3): three build PRs (results and order; tolerances; callbacks and combinators), with `nxx::better_than` and R3 before the tolerances. Phase 1 is re-estimated at 5–7.5 developer-days [est], from step sizes that sum to 4.9–7.7. As on 2026-10-04 (a sum of 6.6–9.6 stated as 6.5–9.5), the sum is rounded to the nearest half day. Phases 1–9 are re-estimated at 44.5–67.5 [est], or 40.5–63.5 without phase 8; phase 3's 6–9 is unchanged. A3 and A4 are already written (§5.3, §6.1) and leave the remaining work (§10.3). + - **Design flaws fixed:** the false "call it alone" hint for a nested `first_of` chain whose inner pair is at fault (item 1); a GCC 16 hard error in `states_accepts_v` for a stage-2 solver whose `accepts_v` is a plain `static bool` (item 1); A1's integer check for `cpp_bin_float_50`, off by one, so that it accepted a threshold of 2·eps (item 11); a possible warning inside the library from `to_scalar`'s third branch (item 9); Appendix D's claim that an overflowing width breaks the order's strict weak ordering, which only a NaN |fx| does (item 8). + - **Counted from the design text:** phase 1 adds 17 deleted declarations with reason texts instead of 39–40, extends the 4 bare-number texts instead of adding 8, and adds 17 compile-fail cases instead of 29. Seven approved detail traits go: `is_bare_solver_v`, `uncurried_v`, `states_accepts_v`, `is_any_rel_v` and `same_estimate_v`, and, to phase 3, `value_fits_v` and `narrows_float_v`. + - **Not taken:** deferring the classified combinators to phase 3 (it would have moved decision 8, and left `std::is_invocable_v` a hard error on misused chains until then); turning reasoned compile-fail cases into concept tests (a reason whose text is not tested can be buried under candidate notes); combinator states with no sibling of their own (§6.10). + --- ## Appendix A: Alternatives considered @@ -2573,7 +2592,7 @@ The prototype's worst case was the `then` contract at 77 lines on GCC; the worst **What the spike changed in the design** (each item is in the section cited): - Floating-point contraction is off inside the headers on Clang, clang-cl and em++ (§5.3, §6.1). -- `better` is a strict weak order, so the static and run-time chains merge to the same best estimate in any fold order (§6.7). The numerics review of the phase-1 core note found that roots' order is not one once a NaN |fx| or an overflowing width enters; the approved R3 order fixes that (§6.7; not built). +- `better` is a strict weak order, so the static and run-time chains merge to the same best estimate in any fold order (§6.7). The numerics review of the phase-1 core note found that roots' order is not one once a NaN |fx| enters; the approved R3 order, NaN last, fixes that (§6.7; not built). The review named an overflowing width as a second cause, but an overflowing width alone only makes wide enclosures tie and does not break the strict weak ordering (corrected on 2026-10-06, §12.21). - The driver's success path is `detail::succeed` with `finish(p, sol) -> optional`, which avoids a GCC 16 false `-Wmaybe-uninitialized` (§6.7). - Brent's `tol1` and stop reasons, so that `criterion` means the width criterion holds (§6.8, §9.3). - The pole check takes its reference from the finite initial samples and rejects a non-finite fx, and its known limit is documented (§7.2). diff --git a/docs/redesign/PLAN.md b/docs/redesign/PLAN.md index 8c2a460..27b5cdc 100644 --- a/docs/redesign/PLAN.md +++ b/docs/redesign/PLAN.md @@ -1,7 +1,7 @@ # Numerixx 2: redesign plan - **Date:** 2026-09-27 -- **Status:** Approved on 2026-09-28, with every default in section 10 (and DESIGN §12) accepted. Phase 0 and the de-risking spike are done. The spike met its 11 exit criteria locally on all 12 presets (DESIGN §10.2 status, Appendix D), passed hosted CI on PR #3 (ci.yml, and the nightly dispatched on the spike branch on 2026-10-02, DESIGN §12.15), and was merged into master on 2026-10-02 (PR #3, 53d3384) with the nightly floor fixes. Phase 1 is the current phase. The spike's code is kept, and phases 1–3 continue from it (DESIGN §10.3). On 2026-10-04 the user approved the phase-1 core design (DESIGN §12.20): one name per quantity and `nxx::best`, one error code for every non-finite input, role-typed mixed tolerances, checked callback results, `nxx::better_than` as the order of failure payloads, and reasoned deletions in the combinators. It is written into DESIGN (mainly §6, §7.1, §7.2 and §10.3) and is not built yet. Phase 1 is re-estimated at 6.5–9.5 developer-days [est] and ends with the tag `v2.0.0-alpha.1`. +- **Status:** Approved on 2026-09-28, with every default in section 10 (and DESIGN §12) accepted. Phase 0 and the de-risking spike are done. The spike met its 11 exit criteria locally on all 12 presets (DESIGN §10.2 status, Appendix D), passed hosted CI on PR #3 (ci.yml, and the nightly dispatched on the spike branch on 2026-10-02, DESIGN §12.15), and was merged into master on 2026-10-02 (PR #3, 53d3384) with the nightly floor fixes. Phase 1 is the current phase. The spike's code is kept, and phases 1–3 continue from it (DESIGN §10.3). On 2026-10-04 the user approved the phase-1 core design (DESIGN §12.20): one name per quantity and `nxx::best`, one error code for every non-finite input, role-typed mixed tolerances, checked callback results, `nxx::better_than` as the order of failure payloads, and reasoned deletions in the combinators. It is written into DESIGN (mainly §6, §7.1, §7.2 and §10.3) and is not built yet. On 2026-10-06 the user accepted every recommendation of a simplicity review of that design (DESIGN §12.21): the same guarantees with fewer deleted declarations, combinator states and traits, built in three PRs. Phase 1 is re-estimated at 5–7.5 developer-days [est] (6.5–9.5 on 2026-10-04) and ends with the tag `v2.0.0-alpha.1`. - **Companion documents:** - [`DESIGN.md`](DESIGN.md): the detailed design reference, covering every decision, the code sketches, per-module algorithm tables, CMake, the test strategy and the full roadmap. Section numbers there are stable; "§n" below refers to them. - [`prototype/`](prototype/): a throwaway feasibility prototype. It compiles and runs on nine configurations: GCC 16 and Clang 22 + libc++, each with and without `-fno-exceptions`; em++ 6.0.8 with `-fexceptions`, `-fno-exceptions` and `-fwasm-exceptions`; MSVC 19.51; and clang-cl 22. @@ -289,7 +289,7 @@ The details are in §10. Sizes are focused developer-days for one developer. |---|---|---|---| | 0 | Skeleton | Tag `v1.0.0` (master, 5de1e07) and `v1.1.0-legacy` (dev-reorg tip, 8528e94) so that existing users can pin the old API; new CMake, presets and every CI leg (including multiprecision); delete the old tree | 2.5–3.5 | | S | **De-risking spike** | Hosted CI green on all legs; the FXT-1 fix pinned; chains with fallible callbacks under clang-cl in CMake builds; CPM deduplication with a parent project in both declaration orders; the umbrella-header compile-time guard and a recorded linalg TU time; your decision on §12 items 1–8; criterion soundness; the canonical calls with run-time inputs; readable compile-fail diagnostics; regularity; derivative composition (11 exit criteria in §10.2; the §12 decisions were made on 2026-09-28) | 3–4 | -| 1 | Core vocabulary | scalar traits and maths helpers, refined types, error and result types, evaluation, `pipes`; plus the core design approved on 2026-10-04 (DESIGN §12.20, §10.3) | 2.5–3.5, re-estimated on 2026-10-04 to 6.5–9.5 [est] | +| 1 | Core vocabulary | scalar traits and maths helpers, refined types, error and result types, evaluation, `pipes`; plus the core design approved on 2026-10-04 and revised on 2026-10-06 (DESIGN §12.20, §12.21, §10.3) | 2.5–3.5, re-estimated on 2026-10-04 to 6.5–9.5 and on 2026-10-06 to 5–7.5 [est] | | 2 | deriv | stencils, steps, `diff`, `diff_with_error`, `ridders`, `mixed`, `derivative_of`, the `numeric` policy | 3–5 | | 3 | Driver + 1-D roots | criteria, driver, combinators, `any_solver` run-time chains, `steps_view`; bisection, Brent, Illinois, Ridders, rtsafe, secant, Newton; expand/scan/subdivide; `solve`, `inverse_of`; Alefeld–Potra–Shi suite | 10–14 | | 4 | optimize (1-D) | golden, Brent-min, bracket_minimum, `maximize`, `minimizer_of` | 2.5–4.5 | @@ -300,7 +300,7 @@ The details are in §10. Sizes are focused developer-days for one developer. | 9 | Multiprecision, docs, release | multiprecision adapter, docs, examples, benchmarks → `v2.0.0` | 5–7 | - **Total for v2.0:** 52.5–77.5 days, or 48.5–73.5 without phase 8. §10.3 shows the arithmetic. -- **After the spike** (decided on 2026-09-29): phases 1–3 continue from the spike's code, with unchanged scope and acceptance criteria, except that phase 1 gained the core design approved on 2026-10-04 (DESIGN §12.20), phase 2 gained the deriv items decided with it, not re-estimated, and phase 3 builds the items that design leaves to it, size unchanged (DESIGN §10.3). About 6.5–9.5 [est, re-estimated on 2026-10-04; 0.5–1 before], 2–3.5 and 6–9 days of them are left, and 46–69.5 days in all for phases 1–9 (42–65.5 without phase 8) [est]. DESIGN §10.3 lists what is done and what is left. The v2.0 total above is the 2026-09-28 plan and leaves out the work added to phase 1. +- **After the spike** (decided on 2026-09-29): phases 1–3 continue from the spike's code, with unchanged scope and acceptance criteria, except that phase 1 gained the core design approved on 2026-10-04 (DESIGN §12.20) and revised on 2026-10-06 (DESIGN §12.21), phase 2 gained the deriv items decided with it, not re-estimated, and phase 3 builds the items that design leaves to it, size unchanged (DESIGN §10.3). About 5–7.5 [est, re-estimated on 2026-10-06; 6.5–9.5 on 2026-10-04; 0.5–1 before], 2–3.5 and 6–9 days of them are left, and 44.5–67.5 days in all for phases 1–9 (40.5–63.5 without phase 8) [est; 46–69.5 and 42–65.5 on 2026-10-04]. DESIGN §10.3 lists what is done and what is left. The v2.0 total above is the 2026-09-28 plan and leaves out the work added to phase 1. - **Order:** deriv comes right after the core vocabulary and before the driver, because it is small, needs only core, and is used by the numeric-derivative Newton in phase 3 and the FD Jacobians in phase 5. Phases 0 → S → 1 → 2 → 3 are sequential. After that, phases 4–7 depend only on core and the driver, so a second developer can run 6 and 7 in parallel with 4 and 5. Phase 8 may follow `v2.0.0`. - **Start fresh on this branch.** Port algorithm bodies mostly from dev-reorg, with provenance noted in each commit. Never merge dev-reorg: it would put 38.8 MB of Blaze into history for good. The `prototype/` headers are the starting point for the core; its in-house LU (`nxx/linalg.hpp`) and zero-heap choices are not carried over (§10.1). - **Migration from 1.x** (§10.4): `MIGRATION.md` maps the old API to the new one. For example: