Skip to content

Commit 37e4805

Browse files
committed
[treeplayer] Fix TTreeFormula vector index into vector-of-vectors
When a vector-type branch was used as the index of a vector-of-vectors branch, TTreeFormula computed the wrong number of instances and emitted out-of-bounds errors. For example, with v2 a vector<int> used as an index: vv1[v2][0] looped over the summed size of all sub-vectors of vv1 instead of the length of v2, printing a spurious extra instance and "index out of bound" errors. vv1[0][v2] silently dropped its last instance. Two issues in the multi-variable-dimension machinery: 1. DefineDimensions registered the inner jagged dimension as a variable dimension even when the outer dimension is selected through a variable "gather" index (-2) and the inner dimension is pinned to a constant. In that configuration the formula does not iterate over the jagged sub-dimension, so the manager must not sum the physical sub-sizes; the number of instances is simply the length of the index variable. 2. GetRealInstance performed a premature out-of-bounds check against the physical sub-size for a variable inner index (-2), using the raw instance number (the index of the index variable) rather than the evaluated index. The real bounds are already checked after evaluating the index variable. Adds regression tests to treeplayer_regressions. Closes #19290 (originally JIRA ROOT-8726). 🤖 Done with the help of [Claude Code](https://claude.com/claude-code) (Claude Opus 4.8)
1 parent ce08cbb commit 37e4805

2 files changed

Lines changed: 119 additions & 6 deletions

File tree

tree/treeplayer/src/TTreeFormula.cxx

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,22 @@ void TTreeFormula::DefineDimensions(Int_t code, Int_t size,
375375
Int_t& virt_dim) {
376376
if (info) {
377377
fManager->EnableMultiVarDims();
378-
//if (fIndexes[code][info->fDim]<0) { // removed because the index might be out of bounds!
378+
// When the primary (first) dimension is selected through a variable index
379+
// (a "gather", fIndexes[code][0]==-2) and this inner variable-size dimension
380+
// is itself pinned to a constant index, the formula does not iterate over the
381+
// jagged sub-dimension: each gathered row yields a single value, so the number
382+
// of instances is simply the length of the index variable. Registering the
383+
// dimension as a variable one would make the manager sum the physical
384+
// sub-sizes (e.g. vv1[v2][0] would loop over the total size of vv1 instead of
385+
// the length of v2). So only register it when it actually varies during the
386+
// iteration. See https://github.com/root-project/root/issues/19290
387+
bool pinnedInnerDuringGather = (fIndexes[code][0] == -2) && (fIndexes[code][fNdimensions[code]] >= 0);
388+
if (!pinnedInnerDuringGather) {
389+
// NOTE: a dimension with a (constant) index is still registered here, because the
390+
// index might be out of bounds and the dimension must still be tracked.
379391
info->fVirtDim = virt_dim;
380392
fManager->AddVarDims(virt_dim); // if (!fVarDims[virt_dim]) fVarDims[virt_dim] = new TArrayI;
381-
//}
393+
}
382394
}
383395

384396
Int_t vsize = 0;
@@ -3631,8 +3643,11 @@ Int_t TTreeFormula::GetRealInstance(Int_t instance, Int_t codeindex) {
36313643
} else {
36323644
local_index = instance;
36333645
}
3634-
if (info && local_index>=fCumulSizes[codeindex][max_dim]) {
3646+
if (info && fIndexes[codeindex][max_dim] != -2 && local_index >= fCumulSizes[codeindex][max_dim]) {
36353647
// We are out of bounds! [Multiple var dims, See same message a few line above]
3648+
// For a variable index (-2) local_index is the instance number of the
3649+
// index variable, not the physical position; the actual bounds are
3650+
// checked below after evaluating the index variable.
36363651
return fNdata[0]+1;
36373652
}
36383653
if (fIndexes[codeindex][max_dim]==-2) {

tree/treeplayer/test/regressions.cxx

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
// ROOT-10702
2828
TEST(TTreeReaderRegressions, CompositeTypeWithNameClash)
2929
{
30-
struct Int { int x; };
30+
struct Int {
31+
int x;
32+
};
3133
gInterpreter->Declare("struct Int { int x; };");
3234

3335
const auto fname = "ttreereader_compositetypewithnameclash.root";
@@ -196,7 +198,7 @@ TEST(TSelectorDrawRegressions, TernaryOperator)
196198
t.Draw("(1?2:3)>>h1(12345,0,20)");
197199
auto h = gROOT->Get<TH1>("h1");
198200
ASSERT_EQ(h->GetXaxis()->GetNbins(), 12345); // was ignored before and set to the default 100
199-
ASSERT_EQ(h->GetBinContent(1235), 1); // FindBin(2) is at 1235
201+
ASSERT_EQ(h->GetBinContent(1235), 1); // FindBin(2) is at 1235
200202
}
201203

202204
// ROOT-4012 (JIRA)
@@ -215,7 +217,10 @@ TEST(TTreeFormulaRegressions, ConstantAlias)
215217
}
216218

217219
// ROOT-8577 (JIRA)
218-
#define MYSTRUCT struct MyS { int x; };
220+
#define MYSTRUCT \
221+
struct MyS { \
222+
int x; \
223+
};
219224
MYSTRUCT
220225
TEST(TTreeFormulaRegressions, WrongName)
221226
{
@@ -661,3 +666,96 @@ TEST(TTreeScan, ULong64Precision)
661666
EXPECT_EQ(scanToString("colsize=21 col=lld:lld:lld"), expectedScanOut);
662667
EXPECT_EQ(scanToString("col=21lld:21lld:21lld"), expectedScanOut);
663668
}
669+
670+
// https://github.com/root-project/root/issues/19290 (originally JIRA ROOT-8726)
671+
// TTreeFormula gave wrong results when a vector-type branch was used as the index
672+
// of a vector-of-vectors branch. For vv1[v2][0] (v2 a vector used as the index of
673+
// the outer dimension of the vector-of-vectors vv1) the number of instances was
674+
// computed as the total/summed size of all the sub-vectors of vv1 instead of the
675+
// length of v2, and out-of-bounds errors were emitted. The companion case
676+
// vv1[0][v2] (constant outer index, vector inner index) silently dropped its last
677+
// instance.
678+
namespace {
679+
680+
// Evaluate a TTreeFormula on the (single) current entry of the tree and return
681+
// all of its instances.
682+
std::vector<double> GH19290EvalAll(TTree &tree, const char *expr)
683+
{
684+
TTreeFormula formula("formula", expr, &tree);
685+
EXPECT_FALSE(formula.GetTree() == nullptr) << "could not compile formula " << expr;
686+
687+
std::vector<double> result;
688+
const Int_t ndata = formula.GetNdata();
689+
result.reserve(ndata);
690+
for (Int_t i = 0; i < ndata; ++i)
691+
result.push_back(formula.EvalInstance(i));
692+
return result;
693+
}
694+
695+
} // namespace
696+
697+
struct GH19290 : public ::testing::Test {
698+
TTree fTree{"t", "t"};
699+
700+
int fN = 1;
701+
// The branch addresses must outlive SetUp(), so keep the objects and the
702+
// pointers used by TTree::Branch as data members.
703+
std::vector<int> fV1{5, 7};
704+
std::vector<std::vector<int>> fVV1{{2, 1}, {3, 4}};
705+
std::vector<int> fV2{0, 1, 0};
706+
std::vector<int> *fV1Ptr = &fV1;
707+
std::vector<std::vector<int>> *fVV1Ptr = &fVV1;
708+
std::vector<int> *fV2Ptr = &fV2;
709+
710+
void SetUp() override
711+
{
712+
gInterpreter->GenerateDictionary("vector<vector<int>>");
713+
714+
fTree.Branch("n", &fN);
715+
fTree.Branch("v1", &fV1Ptr);
716+
fTree.Branch("vv1", &fVV1Ptr);
717+
fTree.Branch("v2", &fV2Ptr);
718+
fTree.Fill();
719+
fTree.GetEntry(0);
720+
}
721+
};
722+
723+
// Sanity: indexing a plain vector with a vector index already worked.
724+
TEST_F(GH19290, VectorIndexIntoVector)
725+
{
726+
// v1[v2] loops over v2 == {0,1,0} -> {v1[0], v1[1], v1[0]}
727+
EXPECT_EQ(GH19290EvalAll(fTree, "v1[v2]"), (std::vector<double>{5, 7, 5}));
728+
}
729+
730+
// Constant outer index, vector inner index. Used to drop the last instance.
731+
TEST_F(GH19290, ConstantOuterVectorInner)
732+
{
733+
// vv1[0][v2] loops over v2 -> {vv1[0][0], vv1[0][1], vv1[0][0]}
734+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[0][v2]"), (std::vector<double>{2, 1, 2}));
735+
}
736+
737+
// Vector outer index, constant inner index. Used to over-count (length was the
738+
// summed size of all sub-vectors) and emit out-of-bounds errors.
739+
TEST_F(GH19290, VectorOuterConstantInner)
740+
{
741+
// vv1[v2][0] loops over v2 -> {vv1[0][0], vv1[1][0], vv1[0][0]}
742+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[v2][0]"), (std::vector<double>{2, 3, 2}));
743+
}
744+
745+
// Scalar indices into a vector of vectors (these always worked).
746+
TEST_F(GH19290, ScalarIndices)
747+
{
748+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[n][0]"), (std::vector<double>{3})); // vv1[1][0]
749+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[0][n]"), (std::vector<double>{1})); // vv1[0][1]
750+
}
751+
752+
// A few related expressions that must keep working (no regressions).
753+
TEST_F(GH19290, NoRegressions)
754+
{
755+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[0]"), (std::vector<double>{2, 1}));
756+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[1]"), (std::vector<double>{3, 4}));
757+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[0][1]"), (std::vector<double>{1}));
758+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1[1][1]"), (std::vector<double>{4}));
759+
EXPECT_EQ(GH19290EvalAll(fTree, "vv1"), (std::vector<double>{2, 1, 3, 4}));
760+
EXPECT_EQ(GH19290EvalAll(fTree, "v1[n]"), (std::vector<double>{7}));
761+
}

0 commit comments

Comments
 (0)