From 570f2e1ec65185c541908879f4bfdd904f750e47 Mon Sep 17 00:00:00 2001 From: Jonas Rembser Date: Sat, 13 Jun 2026 09:52:45 +0200 Subject: [PATCH] [treeplayer] Fix precision loss in TTree::Scan for 64-bit integers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `"lld"` column format passed without an embedded size (e.g. via `"colsize=N col=lld"`) was not recognized as a `"long long"` modifier in **TTreeFormula::PrintValue**, so 64-bit integer branches (e.g. `ULong64_t`) were evaluated as double and rounded above 2^53. This commit fixes a off-by-one problem in the length-modifier detection to address the issue, adding also a regression test. Closes #7844. 🤖 Done with the help of [Claude Code](https://claude.com/claude-code) (Claude Opus 4.8) --- tree/treeplayer/src/TTreeFormula.cxx | 4 +- tree/treeplayer/test/regressions.cxx | 76 ++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/tree/treeplayer/src/TTreeFormula.cxx b/tree/treeplayer/src/TTreeFormula.cxx index fe3c7366ec7ec..ccac1d5fe458c 100644 --- a/tree/treeplayer/src/TTreeFormula.cxx +++ b/tree/treeplayer/src/TTreeFormula.cxx @@ -5052,12 +5052,12 @@ char *TTreeFormula::PrintValue(Int_t mode, Int_t instance, const char *decform) Ssiz_t len = strlen(decform); Char_t outputSizeLevel = 1; char *expo = nullptr; - if (len>2) { + if (len > 1) { switch (decform[len-2]) { case 'l': case 'L': { outputSizeLevel = 2; - if (len>3 && tolower(decform[len-3])=='l') { + if (len > 2 && tolower(decform[len - 3]) == 'l') { outputSizeLevel = 3; } break; diff --git a/tree/treeplayer/test/regressions.cxx b/tree/treeplayer/test/regressions.cxx index c174a9c5c2039..06a0c626eed25 100644 --- a/tree/treeplayer/test/regressions.cxx +++ b/tree/treeplayer/test/regressions.cxx @@ -19,6 +19,7 @@ #include "gtest/gtest.h" +#include #include #include #include @@ -585,3 +586,78 @@ TEST(TTreeScan, TTreeGetBranchOfFriendTChain) throw std::runtime_error("Could not retrieve TTreePlayer from main tree!"); } } + +// https://github.com/root-project/root/issues/7844 +// TTree::Scan() used to lose precision when printing 64-bit integer branches +// (e.g. ULong64_t): the "lld" column format, when given without an embedded +// column size (i.e. via "colsize=N col=lld"), was not recognized as a +// "long long" modifier because of an off-by-one in the length-modifier +// detection in TTreeFormula::PrintValue. As a consequence the value was +// evaluated and printed as a double, rounding anything above 2^53. +TEST(TTreeScan, ULong64Precision) +{ + // The "long long" Scan/Draw column format is evaluated through `long double` + // (see TTreeFormula::PrintValue), so exact 64-bit integer output is only + // possible where `long double` has more mantissa bits than `double`. That is + // the case on x86-64 (80-bit, 64-bit mantissa) but not, e.g., on macOS ARM + // where `long double` is just a 64-bit `double` (53-bit mantissa). Skip the + // exactness check there, since the value genuinely cannot be represented. + if (std::numeric_limits::digits <= std::numeric_limits::digits) + GTEST_SKIP() << "long double is not wider than double here; the 64-bit value " + "is genuinely unrepresentable and exactness cannot be checked"; + + // 1617047019150033926 needs 61 bits, so it cannot be represented exactly + // by a double (53-bit mantissa). + constexpr ULong64_t value{1617047019150033926ULL}; + + constexpr const char *treeName{"tree_7844"}; + ROOT::TestSupport::FileRaii fileGuard{"tree_7844.root"}; + { + std::unique_ptr file{TFile::Open(fileGuard.GetPath().c_str(), "recreate")}; + auto tree = std::make_unique(treeName, treeName); + + ULong64_t x = value; + tree->Branch("x", &x, "x/l"); + tree->Fill(); + file->Write(); + } + + std::unique_ptr file{TFile::Open(fileGuard.GetPath().c_str())}; + auto tree = file->Get(treeName); + + auto *treePlayer = static_cast(tree->GetPlayer()); + ASSERT_TRUE(treePlayer) << "Could not retrieve TTreePlayer from main tree!"; + + ROOT::TestSupport::FileRaii redirectFile{"tree_7844_regression_redirect.txt"}; + // SetScanFileName() stores the raw pointer, so keep the path string alive. + const std::string redirectPath{redirectFile.GetPath()}; + treePlayer->SetScanRedirect(true); + treePlayer->SetScanFileName(redirectPath.c_str()); + + // Run a Scan with the given option string into the redirect file and return + // its contents. + auto scanToString = [&](const char *option) { + tree->Scan("x:x-1617047019150033925:x-1617047019150033000", "", option); + std::ifstream redirectStream(redirectPath.c_str()); + std::stringstream redirectOutput; + redirectOutput << redirectStream.rdbuf(); + return redirectOutput.str(); + }; + + const static std::string expectedScanOut{ + R"Scan(************************************************************************************ +* Row * x * x-1617047019150033925 * x-1617047019150033000 * +************************************************************************************ +* 0 * 1617047019150033926 * 1 * 926 * +************************************************************************************ +)Scan"}; + + // The "long long" column format must print the exact 64-bit value, as well as + // the exact result of integer arithmetic with large constants. The column size + // can either be given separately via "colsize=" (so the format passed to + // TTreeFormula::PrintValue is just "lld") or be embedded in the format token + // itself ("21lld"). Only the former triggered the off-by-one in the + // length-modifier detection, but both must yield the exact output. + EXPECT_EQ(scanToString("colsize=21 col=lld:lld:lld"), expectedScanOut); + EXPECT_EQ(scanToString("col=21lld:21lld:21lld"), expectedScanOut); +}