From c2b1004d5895faf92d526caaee0228b875eb4825 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sun, 1 Mar 2026 23:12:48 +0530 Subject: [PATCH 1/9] Number_types: fix Lazy_exact_nt operator<< to write exact() value Change operator<< for Lazy_exact_nt to output a.exact() instead of to_double(a). The old to_double() conversion loses precision and breaks round-trip save/load of exact kernel coordinates (issue #135). The corresponding operator>> already uses read_float_or_quotient() which handles both floating-point and rational (n/d) formats, so no changes are needed on the input side. Add Lazy_exact_nt_io.cpp regression test verifying round-trip I/O for rationals, integers, negative values, computed sums, large values, and zero. --- .../doc/Number_types/CGAL/Lazy_exact_nt.h | 4 +- Number_types/include/CGAL/Lazy_exact_nt.h | 7 +- Number_types/test/Number_types/CMakeLists.txt | 1 + .../test/Number_types/Lazy_exact_nt_io.cpp | 120 ++++++++++++++++++ 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 Number_types/test/Number_types/Lazy_exact_nt_io.cpp diff --git a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h index 8d225da3b088..2941831e8cf9 100644 --- a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h +++ b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h @@ -111,7 +111,9 @@ static double get_relative_precision_of_to_double(); }; /* end Lazy_exact_nt */ /*! -writes `m` to ostream `out` in an interval format. +writes the exact value `m.exact()` to ostream `out`. +This enables round-trip save/load of `Lazy_exact_nt` values without +loss of precision: `operator>>` reconstructs the same exact number. \relates Lazy_exact_nt */ std::ostream& operator<<(std::ostream& out, const Lazy_exact_nt& m); diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index 32736234fe14..23f02fdeeb9f 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1303,7 +1303,12 @@ const Lazy_exact_nt & y){ template std::ostream & operator<< (std::ostream & os, const Lazy_exact_nt & a) -{ return os << CGAL_NTS to_double(a); } +{ + // Output the exact value so that operator>> can reconstruct the same number. + // Using to_double() (as was done before issue #135 was fixed) loses precision + // and breaks round-trip save/load of exact kernel coordinates. + return os << a.exact(); +} template std::istream & diff --git a/Number_types/test/Number_types/CMakeLists.txt b/Number_types/test/Number_types/CMakeLists.txt index edd7bd5959af..9a0ba807aec6 100644 --- a/Number_types/test/Number_types/CMakeLists.txt +++ b/Number_types/test/Number_types/CMakeLists.txt @@ -30,6 +30,7 @@ create_single_source_cgal_program("Interval_nt_new.cpp") create_single_source_cgal_program("ioformat.cpp") create_single_source_cgal_program("known_bit_size_integers.cpp") create_single_source_cgal_program("Lazy_exact_nt.cpp") +create_single_source_cgal_program("Lazy_exact_nt_io.cpp") create_single_source_cgal_program("Lazy_exact_nt_new.cpp") create_single_source_cgal_program("leda_bigfloat.cpp") create_single_source_cgal_program("leda_bigfloat_interval.cpp") diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp new file mode 100644 index 000000000000..1d2dc5607796 --- /dev/null +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -0,0 +1,120 @@ +// Regression test for issue #135: +// Lazy_exact_nt operator<< used to_double(), losing precision and breaking +// round-trip save/load of exact kernel coordinates. + +#include + +#ifdef CGAL_USE_GMP + +#include +#include + +#include +#include +#include + +typedef CGAL::Lazy_exact_nt Lazy_nt; + +int main() +{ + // Test 1: exact rational round-trip + { + std::cout << "Test 1: round-trip of 1/3" << std::endl; + Lazy_nt a(CGAL::Gmpq(1, 3)); + std::ostringstream oss; + oss << a; + // Should output "1/3", not "0.333333..." + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\", read back equal" << std::endl; + } + + // Test 2: integer value + { + std::cout << "Test 2: round-trip of 42" << std::endl; + Lazy_nt a(42); + std::ostringstream oss; + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 3: negative rational + { + std::cout << "Test 3: round-trip of -5/7" << std::endl; + Lazy_nt a(CGAL::Gmpq(-5, 7)); + std::ostringstream oss; + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 4: value that cannot be represented exactly as double + // 1/3 + 1/7 = 10/21, to_double() would give 0.476190476190... + { + std::cout << "Test 4: round-trip of computed 1/3 + 1/7 = 10/21" << std::endl; + Lazy_nt a(CGAL::Gmpq(1, 3)); + Lazy_nt b(CGAL::Gmpq(1, 7)); + Lazy_nt c = a + b; + std::ostringstream oss; + oss << c; + std::istringstream iss(oss.str()); + Lazy_nt d; + iss >> d; + assert(iss); + assert(c == d); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 5: very large rational that would lose precision as double + { + std::cout << "Test 5: round-trip of large rational" << std::endl; + Lazy_nt a(CGAL::Gmpq(CGAL::Gmpz("99999999999999999999"), CGAL::Gmpz("100000000000000000007"))); + std::ostringstream oss; + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + // Test 6: zero + { + std::cout << "Test 6: round-trip of 0" << std::endl; + Lazy_nt a(0); + std::ostringstream oss; + oss << a; + std::istringstream iss(oss.str()); + Lazy_nt b; + iss >> b; + assert(iss); + assert(a == b); + std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; + } + + std::cout << "All tests passed." << std::endl; + return 0; +} + +#else + +int main() +{ + std::cout << "This test requires GMP support, skipping." << std::endl; + return 0; +} + +#endif // CGAL_USE_GMP From 50138646ad5bad4b1acd2fb6c1707fb218d48b85 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Thu, 16 Apr 2026 22:00:16 +0530 Subject: [PATCH 2/9] Number_types: use Exact_rational in Lazy_exact_nt_io test and assert stream state Addresses review feedback on PR 9431: - Replace CGAL::Gmpq/Gmpz with CGAL::Exact_rational so the test compiles without GMP (per afabri's comment). - Add assert(iss) after every stream extraction to fail loudly on parse errors (per Copilot AI review). --- .../test/Number_types/Lazy_exact_nt_io.cpp | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp index 1d2dc5607796..bfe981c699aa 100644 --- a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -2,25 +2,21 @@ // Lazy_exact_nt operator<< used to_double(), losing precision and breaking // round-trip save/load of exact kernel coordinates. -#include - -#ifdef CGAL_USE_GMP - #include -#include +#include #include #include #include -typedef CGAL::Lazy_exact_nt Lazy_nt; +typedef CGAL::Lazy_exact_nt Lazy_nt; int main() { // Test 1: exact rational round-trip { std::cout << "Test 1: round-trip of 1/3" << std::endl; - Lazy_nt a(CGAL::Gmpq(1, 3)); + Lazy_nt a(CGAL::Exact_rational(1, 3)); std::ostringstream oss; oss << a; // Should output "1/3", not "0.333333..." @@ -49,7 +45,7 @@ int main() // Test 3: negative rational { std::cout << "Test 3: round-trip of -5/7" << std::endl; - Lazy_nt a(CGAL::Gmpq(-5, 7)); + Lazy_nt a(CGAL::Exact_rational(-5, 7)); std::ostringstream oss; oss << a; std::istringstream iss(oss.str()); @@ -64,8 +60,8 @@ int main() // 1/3 + 1/7 = 10/21, to_double() would give 0.476190476190... { std::cout << "Test 4: round-trip of computed 1/3 + 1/7 = 10/21" << std::endl; - Lazy_nt a(CGAL::Gmpq(1, 3)); - Lazy_nt b(CGAL::Gmpq(1, 7)); + Lazy_nt a(CGAL::Exact_rational(1, 3)); + Lazy_nt b(CGAL::Exact_rational(1, 7)); Lazy_nt c = a + b; std::ostringstream oss; oss << c; @@ -80,7 +76,11 @@ int main() // Test 5: very large rational that would lose precision as double { std::cout << "Test 5: round-trip of large rational" << std::endl; - Lazy_nt a(CGAL::Gmpq(CGAL::Gmpz("99999999999999999999"), CGAL::Gmpz("100000000000000000007"))); + CGAL::Exact_rational big; + std::istringstream bigin("99999999999999999999/100000000000000000007"); + bigin >> big; + assert(bigin); + Lazy_nt a(big); std::ostringstream oss; oss << a; std::istringstream iss(oss.str()); @@ -109,12 +109,3 @@ int main() return 0; } -#else - -int main() -{ - std::cout << "This test requires GMP support, skipping." << std::endl; - return 0; -} - -#endif // CGAL_USE_GMP From a3c59ff569b70695b242dbf1e38e770ff6abd5c4 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sat, 29 Aug 2026 18:22:34 +0530 Subject: [PATCH 3/9] Number_types: make Lazy_exact_nt exact output opt-in via IO::set_exact_mode Following review, operator<< no longer changes the default output, which would break existing code that relies on the double output. It keeps to_double() by default, and a per-stream flag set by CGAL::IO::set_exact_mode() switches it to write exact() for a lossless round-trip with operator>> (issue #135). This mirrors set_pretty_mode() (std::ios xalloc/iword). The test sets the mode for the round-trip checks and adds a case confirming the default still writes a double. --- .../doc/Number_types/CGAL/Lazy_exact_nt.h | 32 +++++++++-- Number_types/include/CGAL/Lazy_exact_nt.h | 53 +++++++++++++++++-- .../test/Number_types/Lazy_exact_nt_io.cpp | 22 ++++++++ 3 files changed, 100 insertions(+), 7 deletions(-) diff --git a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h index 2941831e8cf9..ca131dfb7a4e 100644 --- a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h +++ b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h @@ -111,9 +111,11 @@ static double get_relative_precision_of_to_double(); }; /* end Lazy_exact_nt */ /*! -writes the exact value `m.exact()` to ostream `out`. -This enables round-trip save/load of `Lazy_exact_nt` values without -loss of precision: `operator>>` reconstructs the same exact number. +writes `m` to ostream `out`. By default a `double` approximation +(`to_double(m)`) is written, as historically. After calling +`CGAL::IO::set_exact_mode(out)`, the exact value `m.exact()` is written instead, +which enables round-trip save/load of `Lazy_exact_nt` values without loss of +precision: `operator>>` then reconstructs the same exact number. \relates Lazy_exact_nt */ std::ostream& operator<<(std::ostream& out, const Lazy_exact_nt& m); @@ -124,5 +126,29 @@ reads a `NT` from `in`, then converts it to a `Lazy_exact_nt`. */ std::istream& operator>>(std::istream& in, Lazy_exact_nt& m); +namespace IO { + +/*! +makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through their +exact value, so that `operator>>` reconstructs the same number without loss of +precision. Returns the previous state. Mirrors `CGAL::IO::set_pretty_mode()`. +\relates Lazy_exact_nt +*/ +bool set_exact_mode(std::ios& s); + +/*! +makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through +`to_double()`, which is the default. Returns the previous state. +\relates Lazy_exact_nt +*/ +bool set_lossy_mode(std::ios& s); + +/*! +returns `true` if the stream `s` is in exact-output mode for `Lazy_exact_nt`. +\relates Lazy_exact_nt +*/ +bool is_exact_mode(std::ios& s); + +} /* end namespace IO */ } /* end namespace CGAL */ diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index 23f02fdeeb9f..b9a9d6ba99c5 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1300,14 +1300,59 @@ const Lazy_exact_nt & y){ return CGAL::Max > ()(x,y); } +namespace IO { + +// Per-stream flag controlling how Lazy_exact_nt is written by operator<<. By +// default (flag unset) the value is written via to_double(), the historical +// behaviour. After set_exact_mode(s), operator<< writes the exact() value, so +// that operator>> reconstructs the same number without precision loss (issue +// #135). This mirrors set_pretty_mode() / is_pretty(). +namespace internal { +inline int lazy_exact_nt_exact_output_index() +{ + static const int index = std::ios_base::xalloc(); + return index; +} +} // namespace internal + +// Sets the stream s so that operator<< writes Lazy_exact_nt values through their +// exact value (lossless, so operator>> reconstructs the same number). Returns the +// previous state. +inline bool set_exact_mode(std::ios& s) +{ + const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; + s.iword(internal::lazy_exact_nt_exact_output_index()) = 1; + return old; +} + +// Sets the stream s so that operator<< writes Lazy_exact_nt values through +// to_double() (the default). Returns the previous state. +inline bool set_lossy_mode(std::ios& s) +{ + const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; + s.iword(internal::lazy_exact_nt_exact_output_index()) = 0; + return old; +} + +// Returns true if s is in exact-output mode for Lazy_exact_nt. +inline bool is_exact_mode(std::ios& s) +{ + return s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; +} + +} // namespace IO + template std::ostream & operator<< (std::ostream & os, const Lazy_exact_nt & a) { - // Output the exact value so that operator>> can reconstruct the same number. - // Using to_double() (as was done before issue #135 was fixed) loses precision - // and breaks round-trip save/load of exact kernel coordinates. - return os << a.exact(); + // By default output a double, the historical behaviour. After + // CGAL::IO::set_exact_mode(os), output the exact value instead, so that + // operator>> reconstructs the same number without precision loss (issue #135). + // Keeping the default keeps this non-breaking. + if (IO::is_exact_mode(os)) + return os << a.exact(); + return os << CGAL_NTS to_double(a); } template diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp index bfe981c699aa..94e081e10122 100644 --- a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -1,6 +1,10 @@ // Regression test for issue #135: // Lazy_exact_nt operator<< used to_double(), losing precision and breaking // round-trip save/load of exact kernel coordinates. +// +// The default operator<< still writes to_double(). CGAL::IO::set_exact_mode(os) +// switches it to write the exact value, which round-trips losslessly through +// operator>>. This test sets that mode and checks the round-trip is exact. #include #include @@ -18,6 +22,7 @@ int main() std::cout << "Test 1: round-trip of 1/3" << std::endl; Lazy_nt a(CGAL::Exact_rational(1, 3)); std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << a; // Should output "1/3", not "0.333333..." std::istringstream iss(oss.str()); @@ -33,6 +38,7 @@ int main() std::cout << "Test 2: round-trip of 42" << std::endl; Lazy_nt a(42); std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << a; std::istringstream iss(oss.str()); Lazy_nt b; @@ -47,6 +53,7 @@ int main() std::cout << "Test 3: round-trip of -5/7" << std::endl; Lazy_nt a(CGAL::Exact_rational(-5, 7)); std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << a; std::istringstream iss(oss.str()); Lazy_nt b; @@ -64,6 +71,7 @@ int main() Lazy_nt b(CGAL::Exact_rational(1, 7)); Lazy_nt c = a + b; std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << c; std::istringstream iss(oss.str()); Lazy_nt d; @@ -82,6 +90,7 @@ int main() assert(bigin); Lazy_nt a(big); std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << a; std::istringstream iss(oss.str()); Lazy_nt b; @@ -96,6 +105,7 @@ int main() std::cout << "Test 6: round-trip of 0" << std::endl; Lazy_nt a(0); std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); oss << a; std::istringstream iss(oss.str()); Lazy_nt b; @@ -105,6 +115,18 @@ int main() std::cout << " OK: wrote \"" << oss.str() << "\"" << std::endl; } + // Test 7: the default (no set_exact_mode) still writes a double, so existing + // behaviour and user code are unchanged. Issue #135 must remain opt-in. + { + std::cout << "Test 7: default output is still to_double" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + std::ostringstream oss; + oss << a; // no set_exact_mode: lossy, historical behaviour + const std::string s = oss.str(); + assert(s.find('/') == std::string::npos); // a double, not "1/3" + std::cout << " OK: default wrote \"" << s << "\" (double, unchanged)" << std::endl; + } + std::cout << "All tests passed." << std::endl; return 0; } From 299654d67b924b38b397da2d690ac7b7ac98926f Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sun, 30 Aug 2026 10:03:04 +0530 Subject: [PATCH 4/9] Number_types: scope the Lazy_exact_nt exact-output guarantee to exact-serializable backends Following review, set_exact_mode() is lossless only for exact types whose own operator<< writes an exact representation, such as rational types (Exact_rational, Gmpq). For CORE::Expr, whose operator<< writes a decimal approximation, exact mode is not lossless. The documentation (reference header and inline) now states this guarantee precisely instead of claiming universal losslessness, and the test adds a CORE::Expr regression (guarded by CGAL_USE_CORE) checking the default stays a double and exact mode writes exact()'s representation which parses. --- .../doc/Number_types/CGAL/Lazy_exact_nt.h | 14 +++++-- Number_types/include/CGAL/Lazy_exact_nt.h | 14 ++++--- .../test/Number_types/Lazy_exact_nt_io.cpp | 37 ++++++++++++++++++- 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h index ca131dfb7a4e..4c33f9dea7a2 100644 --- a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h +++ b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h @@ -114,8 +114,12 @@ static double get_relative_precision_of_to_double(); writes `m` to ostream `out`. By default a `double` approximation (`to_double(m)`) is written, as historically. After calling `CGAL::IO::set_exact_mode(out)`, the exact value `m.exact()` is written instead, -which enables round-trip save/load of `Lazy_exact_nt` values without loss of -precision: `operator>>` then reconstructs the same exact number. +using `NT`'s own output operator. When `NT` has an exact stream representation, +such as a rational type (`Exact_rational`, `Gmpq`), `operator>>` then reconstructs +the same exact number, so exact values can be saved and reloaded without loss. For +`NT` whose output operator writes an approximation (for example `CORE::Expr`, which +writes a decimal), exact mode writes that approximation and the round-trip is not +exact. \relates Lazy_exact_nt */ std::ostream& operator<<(std::ostream& out, const Lazy_exact_nt& m); @@ -130,8 +134,10 @@ namespace IO { /*! makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through their -exact value, so that `operator>>` reconstructs the same number without loss of -precision. Returns the previous state. Mirrors `CGAL::IO::set_pretty_mode()`. +exact value (`NT`'s output operator). The round-trip through `operator>>` is exact +when `NT` has an exact stream representation, such as a rational type; it is not +exact for `NT` whose output writes an approximation, such as `CORE::Expr`. Returns +the previous state. Mirrors `CGAL::IO::set_pretty_mode()`. \relates Lazy_exact_nt */ bool set_exact_mode(std::ios& s); diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index b9a9d6ba99c5..2713aea33855 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1316,8 +1316,9 @@ inline int lazy_exact_nt_exact_output_index() } // namespace internal // Sets the stream s so that operator<< writes Lazy_exact_nt values through their -// exact value (lossless, so operator>> reconstructs the same number). Returns the -// previous state. +// exact value. operator>> then reconstructs the same number when ET has an exact +// stream representation (e.g. a rational type); this is not exact for ET whose +// operator<< writes an approximation (e.g. CORE::Expr). Returns the previous state. inline bool set_exact_mode(std::ios& s) { const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; @@ -1347,9 +1348,12 @@ std::ostream & operator<< (std::ostream & os, const Lazy_exact_nt & a) { // By default output a double, the historical behaviour. After - // CGAL::IO::set_exact_mode(os), output the exact value instead, so that - // operator>> reconstructs the same number without precision loss (issue #135). - // Keeping the default keeps this non-breaking. + // CGAL::IO::set_exact_mode(os), output the exact value via ET's own operator<<. + // The round-trip through operator>> is exact only when ET has an exact stream + // representation (e.g. a rational type such as Exact_rational or Gmpq); it is + // not exact for ET whose operator<< writes an approximation (e.g. CORE::Expr, + // which writes a decimal). Keeping the default keeps this non-breaking. See + // issue #135. if (IO::is_exact_mode(os)) return os << a.exact(); return os << CGAL_NTS to_double(a); diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp index 94e081e10122..4b7bc416cca0 100644 --- a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -3,15 +3,21 @@ // round-trip save/load of exact kernel coordinates. // // The default operator<< still writes to_double(). CGAL::IO::set_exact_mode(os) -// switches it to write the exact value, which round-trips losslessly through -// operator>>. This test sets that mode and checks the round-trip is exact. +// switches it to write the exact value. For a rational backend (Exact_rational) +// that round-trips losslessly through operator>>; for CORE::Expr, whose operator<< +// writes a decimal approximation, exact mode is not lossless (see Test 8). #include #include +#ifdef CGAL_USE_CORE +#include +#endif + #include #include #include +#include typedef CGAL::Lazy_exact_nt Lazy_nt; @@ -127,6 +133,33 @@ int main() std::cout << " OK: default wrote \"" << s << "\" (double, unchanged)" << std::endl; } +#ifdef CGAL_USE_CORE + // Test 8: CORE::Expr regression. Exact mode is lossless only for backends whose + // operator<< writes an exact representation. CORE::Expr::operator<< writes a + // decimal approximation, so exact mode is NOT lossless for it. We check the + // documented behaviour: the default still writes a double, and exact mode writes + // exact()'s (approximate) representation, which parses. + { + typedef CGAL::Lazy_exact_nt Lazy_core; + std::cout << "Test 8: CORE::Expr exact mode is approximate, not lossless" << std::endl; + Lazy_core a = Lazy_core(1) / Lazy_core(3); // 1/3 + + std::ostringstream def; + def << a; // default: a double, unchanged + assert(def.str().find('/') == std::string::npos); + + std::ostringstream oss; + CGAL::IO::set_exact_mode(oss); + oss << a; // exact mode: CORE::Expr's decimal, parses but is not exact + std::istringstream iss(oss.str()); + Lazy_core b; + iss >> b; + assert(iss); + std::cout << " OK: default \"" << def.str() << "\", exact-mode \"" + << oss.str() << "\" (approximate for CORE::Expr)" << std::endl; + } +#endif + std::cout << "All tests passed." << std::endl; return 0; } From eaf296ede1c1e345fbbfa10c0f198ce56cd5096f Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sun, 30 Aug 2026 10:10:48 +0530 Subject: [PATCH 5/9] Number_types: fix stale comment on the exact-output flag to match the scoped guarantee --- Number_types/include/CGAL/Lazy_exact_nt.h | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index 2713aea33855..e1738017ed8c 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1304,9 +1304,11 @@ namespace IO { // Per-stream flag controlling how Lazy_exact_nt is written by operator<<. By // default (flag unset) the value is written via to_double(), the historical -// behaviour. After set_exact_mode(s), operator<< writes the exact() value, so -// that operator>> reconstructs the same number without precision loss (issue -// #135). This mirrors set_pretty_mode() / is_pretty(). +// behaviour. After set_exact_mode(s), operator<< writes the exact() value; the +// round-trip through operator>> is then exact for ET with an exact stream +// representation such as a rational type, but not for ET whose operator<< writes +// an approximation such as CORE::Expr (issue #135). This mirrors set_pretty_mode() +// / is_pretty(). namespace internal { inline int lazy_exact_nt_exact_output_index() { From c8908249639a3a90f84fd4f591956d785898e157 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sun, 30 Aug 2026 10:16:12 +0530 Subject: [PATCH 6/9] Number_types: test set_lossy_mode and the mode setters' previous-state return --- .../test/Number_types/Lazy_exact_nt_io.cpp | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp index 4b7bc416cca0..f32c394814b0 100644 --- a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -5,7 +5,7 @@ // The default operator<< still writes to_double(). CGAL::IO::set_exact_mode(os) // switches it to write the exact value. For a rational backend (Exact_rational) // that round-trips losslessly through operator>>; for CORE::Expr, whose operator<< -// writes a decimal approximation, exact mode is not lossless (see Test 8). +// writes a decimal approximation, exact mode is not lossless (see Test 9). #include #include @@ -133,15 +133,32 @@ int main() std::cout << " OK: default wrote \"" << s << "\" (double, unchanged)" << std::endl; } + // Test 8: set_lossy_mode reverts to the default double output, and the mode + // setters report the previous state (like set_pretty_mode). + { + std::cout << "Test 8: set_lossy_mode reverts, setters report previous state" << std::endl; + Lazy_nt a(CGAL::Exact_rational(1, 3)); + std::ostringstream oss; + const bool was_exact_before = CGAL::IO::set_exact_mode(oss); // default is lossy + assert(!was_exact_before); + assert(CGAL::IO::is_exact_mode(oss)); + const bool was_exact_after = CGAL::IO::set_lossy_mode(oss); // back to lossy + assert(was_exact_after); + assert(!CGAL::IO::is_exact_mode(oss)); + oss << a; // lossy again: a double, not "1/3" + assert(oss.str().find('/') == std::string::npos); + std::cout << " OK: reverted to \"" << oss.str() << "\"" << std::endl; + } + #ifdef CGAL_USE_CORE - // Test 8: CORE::Expr regression. Exact mode is lossless only for backends whose + // Test 9: CORE::Expr regression. Exact mode is lossless only for backends whose // operator<< writes an exact representation. CORE::Expr::operator<< writes a // decimal approximation, so exact mode is NOT lossless for it. We check the // documented behaviour: the default still writes a double, and exact mode writes // exact()'s (approximate) representation, which parses. { typedef CGAL::Lazy_exact_nt Lazy_core; - std::cout << "Test 8: CORE::Expr exact mode is approximate, not lossless" << std::endl; + std::cout << "Test 9: CORE::Expr exact mode is approximate, not lossless" << std::endl; Lazy_core a = Lazy_core(1) / Lazy_core(3); // 1/3 std::ostringstream def; From fbc2039575c9e40fafbe2a8063317731af0213d3 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Mon, 31 Aug 2026 19:27:41 +0530 Subject: [PATCH 7/9] Number_types: drop the CGAL_USE_CORE guard in the test, CORE is always available (review) --- Number_types/test/Number_types/Lazy_exact_nt_io.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp index f32c394814b0..4eb153cf802d 100644 --- a/Number_types/test/Number_types/Lazy_exact_nt_io.cpp +++ b/Number_types/test/Number_types/Lazy_exact_nt_io.cpp @@ -10,9 +10,7 @@ #include #include -#ifdef CGAL_USE_CORE #include -#endif #include #include @@ -150,7 +148,6 @@ int main() std::cout << " OK: reverted to \"" << oss.str() << "\"" << std::endl; } -#ifdef CGAL_USE_CORE // Test 9: CORE::Expr regression. Exact mode is lossless only for backends whose // operator<< writes an exact representation. CORE::Expr::operator<< writes a // decimal approximation, so exact mode is NOT lossless for it. We check the @@ -175,7 +172,6 @@ int main() std::cout << " OK: default \"" << def.str() << "\", exact-mode \"" << oss.str() << "\" (approximate for CORE::Expr)" << std::endl; } -#endif std::cout << "All tests passed." << std::endl; return 0; From 483027ea38a00053ec6b164ab78c8a297095f323 Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Wed, 2 Sep 2026 19:07:46 +0530 Subject: [PATCH 8/9] Stream_support, Number_types: move the exact-output manipulators to Stream Support Following review, CGAL::IO::set_exact_mode, set_lossy_mode and is_exact_mode are moved from Number_types/Lazy_exact_nt.h into Stream_support's IO/io.h, next to the other stream mode manipulators, and reuse the existing Static flag-index idiom. They are documented there with \ingroup PkgStreamSupportRef and CGAL::IO prefixed \sa links, and are listed in the Stream Support PackageDescription, so the documentation testsuite no longer reports them as missing. Lazy_exact_nt's operator<< keeps calling CGAL::IO::is_exact_mode; Lazy_exact_nt.h already includes CGAL/IO/io.h. Behaviour and the test are unchanged. --- .../doc/Number_types/CGAL/Lazy_exact_nt.h | 27 --------- Number_types/include/CGAL/Lazy_exact_nt.h | 45 --------------- .../doc/Stream_support/PackageDescription.txt | 3 + Stream_support/include/CGAL/IO/io.h | 57 +++++++++++++++++++ 4 files changed, 60 insertions(+), 72 deletions(-) diff --git a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h index 4c33f9dea7a2..b65a98462ac9 100644 --- a/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h +++ b/Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h @@ -130,31 +130,4 @@ reads a `NT` from `in`, then converts it to a `Lazy_exact_nt`. */ std::istream& operator>>(std::istream& in, Lazy_exact_nt& m); -namespace IO { - -/*! -makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through their -exact value (`NT`'s output operator). The round-trip through `operator>>` is exact -when `NT` has an exact stream representation, such as a rational type; it is not -exact for `NT` whose output writes an approximation, such as `CORE::Expr`. Returns -the previous state. Mirrors `CGAL::IO::set_pretty_mode()`. -\relates Lazy_exact_nt -*/ -bool set_exact_mode(std::ios& s); - -/*! -makes `operator<<` write `Lazy_exact_nt` values on the stream `s` through -`to_double()`, which is the default. Returns the previous state. -\relates Lazy_exact_nt -*/ -bool set_lossy_mode(std::ios& s); - -/*! -returns `true` if the stream `s` is in exact-output mode for `Lazy_exact_nt`. -\relates Lazy_exact_nt -*/ -bool is_exact_mode(std::ios& s); - -} /* end namespace IO */ - } /* end namespace CGAL */ diff --git a/Number_types/include/CGAL/Lazy_exact_nt.h b/Number_types/include/CGAL/Lazy_exact_nt.h index e1738017ed8c..a73c34772823 100644 --- a/Number_types/include/CGAL/Lazy_exact_nt.h +++ b/Number_types/include/CGAL/Lazy_exact_nt.h @@ -1300,51 +1300,6 @@ const Lazy_exact_nt & y){ return CGAL::Max > ()(x,y); } -namespace IO { - -// Per-stream flag controlling how Lazy_exact_nt is written by operator<<. By -// default (flag unset) the value is written via to_double(), the historical -// behaviour. After set_exact_mode(s), operator<< writes the exact() value; the -// round-trip through operator>> is then exact for ET with an exact stream -// representation such as a rational type, but not for ET whose operator<< writes -// an approximation such as CORE::Expr (issue #135). This mirrors set_pretty_mode() -// / is_pretty(). -namespace internal { -inline int lazy_exact_nt_exact_output_index() -{ - static const int index = std::ios_base::xalloc(); - return index; -} -} // namespace internal - -// Sets the stream s so that operator<< writes Lazy_exact_nt values through their -// exact value. operator>> then reconstructs the same number when ET has an exact -// stream representation (e.g. a rational type); this is not exact for ET whose -// operator<< writes an approximation (e.g. CORE::Expr). Returns the previous state. -inline bool set_exact_mode(std::ios& s) -{ - const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; - s.iword(internal::lazy_exact_nt_exact_output_index()) = 1; - return old; -} - -// Sets the stream s so that operator<< writes Lazy_exact_nt values through -// to_double() (the default). Returns the previous state. -inline bool set_lossy_mode(std::ios& s) -{ - const bool old = s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; - s.iword(internal::lazy_exact_nt_exact_output_index()) = 0; - return old; -} - -// Returns true if s is in exact-output mode for Lazy_exact_nt. -inline bool is_exact_mode(std::ios& s) -{ - return s.iword(internal::lazy_exact_nt_exact_output_index()) != 0; -} - -} // namespace IO - template std::ostream & operator<< (std::ostream & os, const Lazy_exact_nt & a) diff --git a/Stream_support/doc/Stream_support/PackageDescription.txt b/Stream_support/doc/Stream_support/PackageDescription.txt index 7875657b1e57..717835144b7e 100644 --- a/Stream_support/doc/Stream_support/PackageDescription.txt +++ b/Stream_support/doc/Stream_support/PackageDescription.txt @@ -102,10 +102,13 @@ the printing mode. - `CGAL::IO::get_mode()` - `CGAL::IO::is_ascii()` - `CGAL::IO::is_binary()` +- `CGAL::IO::is_exact_mode()` - `CGAL::IO::is_pretty()` - `CGAL::IO::set_mode()` - `CGAL::IO::set_ascii_mode()` - `CGAL::IO::set_binary_mode()` +- `CGAL::IO::set_exact_mode()` +- `CGAL::IO::set_lossy_mode()` - `CGAL::IO::set_pretty_mode()` - \link IOstreamOperators `CGAL::operator>>()` \endlink - \link IOstreamOperators `CGAL::operator<<()` \endlink diff --git a/Stream_support/include/CGAL/IO/io.h b/Stream_support/include/CGAL/IO/io.h index 86041bfaec45..12c6ac2db234 100644 --- a/Stream_support/include/CGAL/IO/io.h +++ b/Stream_support/include/CGAL/IO/io.h @@ -47,6 +47,12 @@ class Static static const int mode = std::ios::xalloc(); return mode; } + + static int get_exact_mode() + { + static const int exact_mode = std::ios::xalloc(); + return exact_mode; + } }; /*! @@ -654,6 +660,57 @@ checks if the %IO stream `s` is in `BINARY` mode. */ inline bool is_binary(std::ios& s) { return s.iword(Static::get_mode()) == BINARY; } +/*! +\ingroup PkgStreamSupportRef + +sets the stream `s` so that a number type with a lazy exact representation, such as +`CGAL::Lazy_exact_nt`, is written through its exact value instead of through a +`double` approximation. Returns the previous state. + +The round-trip through `operator>>` is exact when the exact type has an exact stream +representation, such as a rational type. It is not exact for an exact type whose +output operator writes an approximation, such as `CORE::Expr`. + +\sa `CGAL::IO::set_lossy_mode()` +\sa `CGAL::IO::is_exact_mode()` +\sa `CGAL::IO::set_pretty_mode()` +*/ +inline bool set_exact_mode(std::ios& s) +{ + const bool old = s.iword(Static::get_exact_mode()) != 0; + s.iword(Static::get_exact_mode()) = 1; + return old; +} + +/*! +\ingroup PkgStreamSupportRef + +sets the stream `s` so that a number type with a lazy exact representation, such as +`CGAL::Lazy_exact_nt`, is written through a `double` approximation. This is the +default. Returns the previous state. + +\sa `CGAL::IO::set_exact_mode()` +\sa `CGAL::IO::is_exact_mode()` +\sa `CGAL::IO::set_pretty_mode()` +*/ +inline bool set_lossy_mode(std::ios& s) +{ + const bool old = s.iword(Static::get_exact_mode()) != 0; + s.iword(Static::get_exact_mode()) = 0; + return old; +} + +/*! +\ingroup PkgStreamSupportRef + +returns `true` if the stream `s` is in exact-output mode, that is if +`CGAL::IO::set_exact_mode()` was called on it. + +\sa `CGAL::IO::set_exact_mode()` +\sa `CGAL::IO::set_lossy_mode()` +*/ +inline bool is_exact_mode(std::ios& s) { return s.iword(Static::get_exact_mode()) != 0; } + } // namespace IO template < class T > From 8ccae13b325bb0756c96f08d5256577d13ef6c8f Mon Sep 17 00:00:00 2001 From: Rajdeep Singh Date: Sat, 5 Sep 2026 04:37:25 +0530 Subject: [PATCH 9/9] Stream_support: add Number_types doc dependency, document the default per type The documentation of set_exact_mode() and set_lossy_mode() links to CGAL::Lazy_exact_nt and CGAL::Gmpq, which live in Number_types, so add Number_types to the doc dependencies of Stream_support for those links to resolve. Also state explicitly which types consult the flag and what the default is for each: the flag is only read by number types whose default output is an approximation, currently CGAL::Lazy_exact_nt, while non lazy exact types such as CGAL::Gmpq always write their exact representation and ignore it. --- Stream_support/doc/Stream_support/dependencies | 1 + Stream_support/include/CGAL/IO/io.h | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Stream_support/doc/Stream_support/dependencies b/Stream_support/doc/Stream_support/dependencies index 48f34988e106..45895f3a4955 100644 --- a/Stream_support/doc/Stream_support/dependencies +++ b/Stream_support/doc/Stream_support/dependencies @@ -8,6 +8,7 @@ Manual Mesh_2 Mesh_3 Nef_3 +Number_types Point_set_3 Point_set_processing_3 Polygon diff --git a/Stream_support/include/CGAL/IO/io.h b/Stream_support/include/CGAL/IO/io.h index 12c6ac2db234..067ae9a2b4bc 100644 --- a/Stream_support/include/CGAL/IO/io.h +++ b/Stream_support/include/CGAL/IO/io.h @@ -667,6 +667,13 @@ sets the stream `s` so that a number type with a lazy exact representation, such `CGAL::Lazy_exact_nt`, is written through its exact value instead of through a `double` approximation. Returns the previous state. +This flag is only consulted by number types whose default output is an +approximation. At present that is `CGAL::Lazy_exact_nt`, which by default writes a +`double`. Exact number types that are not lazy, such as `CGAL::Gmpq` or +`CGAL::Exact_rational`, always write their exact representation and ignore this +flag. The default output is therefore exact for those types and approximate for +`CGAL::Lazy_exact_nt`. + The round-trip through `operator>>` is exact when the exact type has an exact stream representation, such as a rational type. It is not exact for an exact type whose output operator writes an approximation, such as `CORE::Expr`. @@ -687,7 +694,8 @@ inline bool set_exact_mode(std::ios& s) sets the stream `s` so that a number type with a lazy exact representation, such as `CGAL::Lazy_exact_nt`, is written through a `double` approximation. This is the -default. Returns the previous state. +default for `CGAL::Lazy_exact_nt`. Number types that always write their exact +representation, such as `CGAL::Gmpq`, ignore this flag. Returns the previous state. \sa `CGAL::IO::set_exact_mode()` \sa `CGAL::IO::is_exact_mode()`