Skip to content

Number_types: fix Lazy_exact_nt operator<< to write exact() value - #9431

Open
RajdeepKushwaha5 wants to merge 9 commits into
CGAL:mainfrom
RajdeepKushwaha5:fix/lazy-exact-nt-ostream-exact
Open

Number_types: fix Lazy_exact_nt operator<< to write exact() value#9431
RajdeepKushwaha5 wants to merge 9 commits into
CGAL:mainfrom
RajdeepKushwaha5:fix/lazy-exact-nt-ostream-exact

Conversation

@RajdeepKushwaha5

@RajdeepKushwaha5 RajdeepKushwaha5 commented Apr 15, 2026

Copy link
Copy Markdown
Member

Summary of Changes

operator<< for Lazy_exact_nt<ET> writes to_double(a), which loses precision and breaks round-trip save and load of exact kernel coordinates. Writing Lazy_exact_nt<Exact_rational>(1/3) outputs 0.333333, and reading it back gives a
different exact value. This is issue #135.

Changing that unconditionally is a breaking change, so following the stream modifier solution suggested by Laurent in the issue and requested by @sloriot, the default is left untouched and the exact output is opt-in per stream:

std::ostringstream os;
os << a;                     // 0.333333, the historical behaviour, unchanged
CGAL::IO::set_exact_mode(os);
os << a;                     // 1/3, and operator>> reads back the same exact value

The three manipulators live in Stream Support next to the other stream mode manipulators and reuse the same std::ios::xalloc and iword idiom as set_pretty_mode.

The lossless round-trip holds when ET has an exact stream representation, such as a rational type. It does not hold for an ET whose own output operator writes an approximation, for example CORE::Expr, which writes a decimal. That limit is
documented and covered by a test.

Changes

  • Stream_support/include/CGAL/IO/io.h: added CGAL::IO::set_exact_mode(), CGAL::IO::set_lossy_mode() and CGAL::IO::is_exact_mode(), documented with \ingroup PkgStreamSupportRef.
  • Stream_support/doc/Stream_support/PackageDescription.txt: registered the three functions.
  • Stream_support/doc/Stream_support/dependencies: added Number_types so the CGAL::Lazy_exact_nt and CGAL::Gmpq links resolve.
  • Number_types/include/CGAL/Lazy_exact_nt.h: operator<< writes a.exact() when the stream is in exact mode and to_double(a) otherwise, which stays the default.
  • Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h: documented the behaviour and the limits of the round-trip guarantee.
  • Number_types/test/Number_types/Lazy_exact_nt_io.cpp and CMakeLists.txt: nine cases covering rationals, integers, negatives, computed sums, large values, zero, the unchanged default, set_lossy_mode with the previous state return of the
    setters, and the CORE::Expr behaviour.

Release Management

Copilot AI review requested due to automatic review settings April 15, 2026 00:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes Lazy_exact_nt<ET> stream output so that saving/loading exact values round-trips without precision loss, addressing issue #135 in the Number_types package.

Changes:

  • Update operator<< for Lazy_exact_nt<ET> to write a.exact() instead of to_double(a).
  • Update documentation to describe the new exact output behavior.
  • Add and register a regression test that exercises round-trip I/O for several rational/integer cases.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
Number_types/include/CGAL/Lazy_exact_nt.h Changes streaming output to use the exact value (exact()), enabling lossless round-trip with existing operator>>.
Number_types/doc/Number_types/CGAL/Lazy_exact_nt.h Updates docs to match the new exact-output semantics and round-trip intent.
Number_types/test/Number_types/Lazy_exact_nt_io.cpp Adds regression coverage for round-trip I/O of exact rationals/integers (including large values).
Number_types/test/Number_types/CMakeLists.txt Registers the new single-source test program.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +36 to +39
std::istringstream iss(oss.str());
Lazy_nt b;
iss >> b;
assert(a == b);

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: the test should assert the stream extraction succeeded after iss >> b to avoid false positives if parsing fails.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +52
std::istringstream iss(oss.str());
Lazy_nt b;
iss >> b;
assert(a == b);

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: please assert the stream is still good after iss >> b so the test can’t pass if parsing fails.

Copilot uses AI. Check for mistakes.
oss << c;
std::istringstream iss(oss.str());
Lazy_nt d;
iss >> d;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: add a stream-state assertion after iss >> d to ensure the read succeeded before comparing c == d.

Suggested change
iss >> d;
iss >> d;
assert(iss);

Copilot uses AI. Check for mistakes.
Comment on lines +78 to +81
std::istringstream iss(oss.str());
Lazy_nt b;
iss >> b;
assert(a == b);

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: assert successful extraction after iss >> b so the test can’t pass if parsing fails.

Copilot uses AI. Check for mistakes.
oss << a;
std::istringstream iss(oss.str());
Lazy_nt b;
iss >> b;

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: because the expected value is 0 here, a failed parse could leave b at the default value and make the assertion pass. Add an assert(iss) / assert(!iss.fail()) after iss >> b.

Suggested change
iss >> b;
iss >> b;
assert(iss);

Copilot uses AI. Check for mistakes.
Comment thread Number_types/test/Number_types/Lazy_exact_nt_io.cpp Outdated
Comment on lines +23 to +26
std::istringstream iss(oss.str());
Lazy_nt b;
iss >> b;
assert(a == b);

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test doesn’t verify that extraction succeeded (stream state) before comparing values. If parsing fails, b can remain at its default value (which is 0 for Lazy_exact_nt), potentially making the test pass spuriously (notably in the zero case). Add an assertion like assert(iss) / assert(!iss.fail()) after iss >> b (and similarly in the other test blocks).

Copilot uses AI. Check for mistakes.
@RajdeepKushwaha5
RajdeepKushwaha5 force-pushed the fix/lazy-exact-nt-ostream-exact branch from 56d312f to a935007 Compare April 15, 2026 01:08
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 CGAL#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.
…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).
@RajdeepKushwaha5
RajdeepKushwaha5 force-pushed the fix/lazy-exact-nt-ostream-exact branch from 78773d6 to 5013864 Compare May 5, 2026 06:46
@MaelRL MaelRL added this to the 6.3-beta milestone Jun 3, 2026
@sloriot

sloriot commented Aug 26, 2026

Copy link
Copy Markdown
Member

This is not a breaking change I'm willing to have. The only option I would validate is the stream modifier solution suggested by Laurent in the issue.

@sloriot sloriot added the Not yet approved The feature or pull-request has not yet been approved. label Aug 26, 2026
…t_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 CGAL#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.
@RajdeepKushwaha5

Copy link
Copy Markdown
Member Author

I have reworked as discussed. operator<< keeps to_double() by default, so nothing breaks, and CGAL::IO::set_exact_mode(os) switches it to write exact() for a lossless round-trip with operator>> (issue #135), mirroring set_pretty_mode via 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. I named them set_exact_mode / set_lossy_mode / is_exact_mode; happy to rename if you prefer.

@fallenmi fallenmi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_exact_mode() is not lossless for every supported Lazy_exact_nt backend. The new branch delegates to a.exact()'s stream operator, but CORE::Expr::operator<< writes a decimal approximation. CORE::Expr is a shipped CGAL number type, and the existing Number_types suite instantiates Lazy_exact_nt<CORE::Expr> in its CORE+LEDA configuration.

I reproduced this on exact head a3c59ff and current merge 54a86f0 with the exact value 1/3. At stream precisions 6, 18, and 80, exact mode wrote 0.333333, 0.33333333333333333, and 0.33333333333333333333333333; every value parsed successfully, but every restored value compared unequal to the original exact value. In contrast, the submitted Exact_rational path writes 1/3 and round-trips correctly, and the historical default remains 0.333333.

Please either make exact-mode serialization round-trip for the supported exact backends, or constrain/guard the API and documentation to the backends for which that guarantee is valid, and add a CORE::Expr regression. The exact-head CI checks are otherwise green; the new focused target and the existing ioformat/Exact_rational tests pass locally.

Reviewed with OpenAI Codex assistance; I independently traced the supported backend stream operators and reproduced the base, exact-head, and current-merge behavior locally.

…-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.
@RajdeepKushwaha5

Copy link
Copy Markdown
Member Author

Thank you for the careful review and the reproduction, you were right. I confirmed it on a CORE build: Lazy_exact_nt<CORE::Expr> in exact mode writes CORE::Expr's decimal, which parses but is not equal to the original. Lossless round-trip only holds for backends whose own operator<< is exact, such as rational types.

I have scoped the guarantee accordingly: the reference documentation and the inline comments now state that exact mode is lossless only when NT has an exact stream representation (a rational type), and not for types like CORE::Expr. I added a
CORE::Expr regression (guarded by CGAL_USE_CORE) that checks the default still writes a double and that exact mode's output parses, and I added coverage for set_lossy_mode and the setters' previous-state return. The Exact_rational
round-trip tests are unchanged and still pass, and I verified compilation both with and without CORE.

#include <CGAL/Lazy_exact_nt.h>
#include <CGAL/Exact_rational.h>

#ifdef CGAL_USE_CORE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for that macro anymore, it is always available

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed the CGAL_USE_CORE guard so the CORE::Expr test is unconditional. It builds and passes, and it is registered the same way as the other CORE tests.
Thank you.

@sloriot

sloriot commented Aug 31, 2026

Copy link
Copy Markdown
Member

The behavior I see in the test is what I would have expect. Let's test it.

@fallenmi fallenmi left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — this addresses my prior blocker. At exact head fbc2039575c9e40fafbe2a8063317731af0213d3, the API and documentation now scope the lossless guarantee to backends with an exact stream representation, and the CORE::Expr regression is unconditional as requested by the maintainer.

I rechecked the submitted nine cases and the focused three-precision CORE oracle on this head and the current clean merge 330f425a07296152f9d406fc1a7c9a87609d8b3d; parsing succeeds and the documented lossy CORE behavior is preserved, while Exact_rational still round-trips exactly. The changed header, documentation, and test blobs are identical in the merge. All eight substantive check runs are successful; the remaining check is an intentional skipped label-removal job.

Disclosure: I used OpenAI Codex to assist this changed-head review. I verified the exact commits, current merge, source paths, interactions, and live CI before submitting.

@afabri

afabri commented Sep 1, 2026

Copy link
Copy Markdown
Member

You still have to add the functions in the package Stream Support . The documentation testsuite reports that they are missing. To get the doxygen links right you must prefix them also with CGAL::IO

…tream 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.
@RajdeepKushwaha5

Copy link
Copy Markdown
Member Author

I have moved set_exact_mode, set_lossy_mode and is_exact_mode into Stream Support (CGAL/IO/io.h), next to the other stream mode manipulators, reusing 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.txt, so the documentation testsuite should no longer report them as missing. Lazy_exact_nt's operator<< still calls CGAL::IO::is_exact_mode and behaviour is unchanged; the test passes.

@afabri

afabri commented Sep 2, 2026

Copy link
Copy Markdown
Member

/build:v0

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

The documentation is built. It will be available, after a few minutes, here: https://cgal.github.io/9431/v0/Manual/index.html

@afabri

afabri commented Sep 3, 2026

Copy link
Copy Markdown
Member

I am surprised that in the documentation of CGAL::IO::Mode we do not document the entries of the enum.

Concerning the lossy mode, I am wondering if we want that only for Lazy_exact_nt or also for other exact types like Exact_rational etc. We have to document what is the default then, as for now it is not the same for these two.

As the link to Lazy_exact_nt is missing in the documentation, you probably have to add Number_types to the file dependencies

… 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.
@RajdeepKushwaha5

Copy link
Copy Markdown
Member Author

@afabri Thanks for the review. Pushed in 8ccae13.

Number_types in dependencies

Added. The documentation of set_exact_mode() and set_lossy_mode() refers to CGAL::Lazy_exact_nt and CGAL::Gmpq, both of which live in Number_types, so those links should resolve now. Number_types does not list Stream_support in its
own dependencies, so this does not introduce a cycle. In the v0 build the three functions do show up, but Lazy_exact_nt is still plain text, which is the missing link you saw. That build predates 8ccae13, so could you run /build once more to
confirm the link resolves?

The default, and whether the mode should apply to other exact types

You are right that the default was not stated per type. I have now documented it explicitly on both functions: the flag is only consulted by number types whose default output is an approximation, which today means CGAL::Lazy_exact_nt, while
non lazy exact types such as CGAL::Gmpq and CGAL::Exact_rational always write their exact representation and ignore it. So the default is exact for those types and approximate for Lazy_exact_nt. I checked this rather than assuming it:

stream value output
Exact_rational, default 1/3 1/3
Exact_rational, after set_lossy_mode 1/3 1/3
Lazy_exact_nt, default 1/3 0.333333
Lazy_exact_nt, after set_exact_mode 1/3 1/3

On whether to extend the mode, my suggestion is to keep it to lazy types in this PR. set_exact_mode() is already a no-op for Gmpq and Exact_rational because they write their exact value anyway, so there is nothing to switch on for them. The only genuine extension is the opposite direction, making set_lossy_mode() force an exact type to write a double. That is a new feature rather than a fix for #135, and it would mean touching the output operator of every exact number type and giving each one its own test, so I would rather propose it separately. If you prefer it inside this PR, tell me which types you want covered and I will add them.

Documentation of the Mode enum entries

You are right, ASCII, PRETTY and BINARY have no individual descriptions. The enum carries a block comment but no per entry documentation. This is pre-existing, it goes back to the commit that merged iostream into Stream_support, and this branch does not touch the enum. I am happy to write the three entry descriptions, either here or as a separate small PR. Let me know which you prefer.

One process question

Since the manipulators now live in Stream Support, this PR adds three public functions rather than only fixing Lazy_exact_nt. I have updated the description to list Stream Support as an affected package and to name them under Feature/Small Feature, with the link to the documentation you built. Tell me if you want this to go through the small feature procedure and I will open the wiki page for it.

@sloriot

sloriot commented Sep 9, 2026

Copy link
Copy Markdown
Member

Successfully tested in CGAL-6.3-Ic-73

@sloriot

sloriot commented Sep 9, 2026

Copy link
Copy Markdown
Member

@afabri @MaelRL do you validate the choice of names?

  • set_lossy_mode()
  • set_exact_mode()
  • is_exact_mode()

@sloriot

sloriot commented Sep 9, 2026

Copy link
Copy Markdown
Member

@RajdeepKushwaha5 I think it would be better to write a small feature page on the wiki so that naming could be discussed and approve following our guidelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Not yet approved The feature or pull-request has not yet been approved. Pkg::Number_types Tested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

operator<< (std::ostream & os, const Lazy_exact_nt<ET> & a) should insert a.exact() in the stream

6 participants