From ad4b916a2d6e7b8cc730a21e490faf2286d296e6 Mon Sep 17 00:00:00 2001 From: Dmitry Chirkov Date: Mon, 26 Oct 2026 19:07:37 -0700 Subject: [PATCH 1/3] GH-55277: [C++][Gandiva] Add regression for decimal cast stack overflow Add a large-batch decimal cast test to reproduce stack exhaustion with JIT optimization disabled. AI tools helped investigate the crash and write the test. --- cpp/src/gandiva/tests/decimal_test.cc | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/cpp/src/gandiva/tests/decimal_test.cc b/cpp/src/gandiva/tests/decimal_test.cc index 043bdc4605a7..e395452d828c 100644 --- a/cpp/src/gandiva/tests/decimal_test.cc +++ b/cpp/src/gandiva/tests/decimal_test.cc @@ -67,6 +67,42 @@ std::vector TestDecimal::MakeDecimalVector(std::vector return ret; } +TEST_F(TestDecimal, CastLargeBatch) { + // Allocating decimal return slots in the record loop can exhaust the stack + // when optimization is disabled. A single int32-to-decimal cast is sufficient. + constexpr int kNumRecords = 1 << 19; + auto input_field = arrow::field("large_batch_decimal", arrow::int32()); + auto schema = arrow::schema({input_field}); + auto decimal_type = arrow::decimal128(10, 0); + auto decimal = TreeExprBuilder::MakeFunction( + "castDECIMAL", {TreeExprBuilder::MakeField(input_field)}, decimal_type); + auto expression = + TreeExprBuilder::MakeExpression(decimal, arrow::field("result", decimal_type)); + + std::vector values(kNumRecords); + std::vector validity(kNumRecords); + std::vector expected_values(kNumRecords); + for (int i = 0; i < kNumRecords; ++i) { + values[i] = i % 17 - 8; + validity[i] = i % 11 != 0; + expected_values[i] = Decimal128(values[i]); + } + auto batch = arrow::RecordBatch::Make(schema, kNumRecords, + {MakeArrowArrayInt32(values, validity)}); + auto expected = MakeArrowArrayDecimal(decimal_type, expected_values, validity); + + for (bool optimize : {false, true}) { + SCOPED_TRACE(optimize); + auto config = std::make_shared(optimize); + std::shared_ptr projector; + ASSERT_OK(Projector::Make(schema, {expression}, config, &projector)); + arrow::ArrayVector output; + ASSERT_OK(projector->Evaluate(*batch, pool_, &output)); + ASSERT_EQ(output.size(), 1); + EXPECT_ARROW_ARRAY_EQUALS(expected, output[0]); + } +} + TEST_F(TestDecimal, TestSimple) { // schema for input fields constexpr int32_t precision = 36; From 47578383648e09535213ee9598ba250ded672037 Mon Sep 17 00:00:00 2001 From: Dmitry Chirkov Date: Mon, 26 Oct 2026 19:25:49 -0700 Subject: [PATCH 2/3] GH-55277: [C++][Gandiva] Allocate decimal return slots in the entry block --- cpp/src/gandiva/decimal_ir.cc | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/cpp/src/gandiva/decimal_ir.cc b/cpp/src/gandiva/decimal_ir.cc index 36e423e0647c..4286f8d21850 100644 --- a/cpp/src/gandiva/decimal_ir.cc +++ b/cpp/src/gandiva/decimal_ir.cc @@ -246,9 +246,10 @@ DecimalIR::ValueWithOverflow DecimalIR::AddWithOverflowCheck(const ValueFull& x, // This is pretty complex, so use CPP fns. llvm::Value* DecimalIR::AddLarge(const ValueFull& x, const ValueFull& y, const ValueFull& out) { - auto block = ir_builder()->GetInsertBlock(); - auto out_high_ptr = new llvm::AllocaInst(types()->i64_type(), 0, "out_hi", block); - auto out_low_ptr = new llvm::AllocaInst(types()->i64_type(), 0, "out_low", block); + auto& entry = ir_builder()->GetInsertBlock()->getParent()->getEntryBlock(); + llvm::IRBuilder<> entry_builder(&entry, entry.begin()); + auto out_high_ptr = entry_builder.CreateAlloca(types()->i64_type(), nullptr, "out_hi"); + auto out_low_ptr = entry_builder.CreateAlloca(types()->i64_type(), nullptr, "out_low"); auto x_split = ValueSplit::MakeFromInt128(this, x.value()); auto y_split = ValueSplit::MakeFromInt128(this, y.value()); @@ -454,9 +455,12 @@ llvm::Value* DecimalIR::CallDecimalFunction(const std::string& function_name, llvm::Value* result = nullptr; if (return_type == i128) { // for i128 ret, replace with two int64* args, and join them. - auto block = ir_builder()->GetInsertBlock(); - auto out_high_ptr = new llvm::AllocaInst(i64, 0, "out_hi", block); - auto out_low_ptr = new llvm::AllocaInst(i64, 0, "out_low", block); + // Decimal calls can be emitted inside the record loop or a CASE branch. + // Allocate return slots once per invocation, rather than once per row. + auto& entry = ir_builder()->GetInsertBlock()->getParent()->getEntryBlock(); + llvm::IRBuilder<> entry_builder(&entry, entry.begin()); + auto out_high_ptr = entry_builder.CreateAlloca(i64, nullptr, "out_hi"); + auto out_low_ptr = entry_builder.CreateAlloca(i64, nullptr, "out_low"); dis_assembled_args.push_back(out_high_ptr); dis_assembled_args.push_back(out_low_ptr); From 0d9e18e3fe03a4c6d4af46e6d4d2e8646a9c1249 Mon Sep 17 00:00:00 2001 From: Dmitry Chirkov Date: Tue, 27 Oct 2026 20:34:41 -0700 Subject: [PATCH 3/3] Refactor decimal return-slot allocation into a shared entry-block helper --- cpp/src/gandiva/decimal_ir.cc | 16 +++++++++------- cpp/src/gandiva/decimal_ir.h | 4 ++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cpp/src/gandiva/decimal_ir.cc b/cpp/src/gandiva/decimal_ir.cc index 4286f8d21850..5ece2bf9e81a 100644 --- a/cpp/src/gandiva/decimal_ir.cc +++ b/cpp/src/gandiva/decimal_ir.cc @@ -243,13 +243,18 @@ DecimalIR::ValueWithOverflow DecimalIR::AddWithOverflowCheck(const ValueFull& x, return ValueWithOverflow(sum_descaled, overflow); } -// This is pretty complex, so use CPP fns. -llvm::Value* DecimalIR::AddLarge(const ValueFull& x, const ValueFull& y, - const ValueFull& out) { +std::pair DecimalIR::CreateEntryBlockReturnSlots() { auto& entry = ir_builder()->GetInsertBlock()->getParent()->getEntryBlock(); llvm::IRBuilder<> entry_builder(&entry, entry.begin()); auto out_high_ptr = entry_builder.CreateAlloca(types()->i64_type(), nullptr, "out_hi"); auto out_low_ptr = entry_builder.CreateAlloca(types()->i64_type(), nullptr, "out_low"); + return {out_high_ptr, out_low_ptr}; +} + +// This is pretty complex, so use CPP fns. +llvm::Value* DecimalIR::AddLarge(const ValueFull& x, const ValueFull& y, + const ValueFull& out) { + auto [out_high_ptr, out_low_ptr] = CreateEntryBlockReturnSlots(); auto x_split = ValueSplit::MakeFromInt128(this, x.value()); auto y_split = ValueSplit::MakeFromInt128(this, y.value()); @@ -457,10 +462,7 @@ llvm::Value* DecimalIR::CallDecimalFunction(const std::string& function_name, // for i128 ret, replace with two int64* args, and join them. // Decimal calls can be emitted inside the record loop or a CASE branch. // Allocate return slots once per invocation, rather than once per row. - auto& entry = ir_builder()->GetInsertBlock()->getParent()->getEntryBlock(); - llvm::IRBuilder<> entry_builder(&entry, entry.begin()); - auto out_high_ptr = entry_builder.CreateAlloca(i64, nullptr, "out_hi"); - auto out_low_ptr = entry_builder.CreateAlloca(i64, nullptr, "out_low"); + auto [out_high_ptr, out_low_ptr] = CreateEntryBlockReturnSlots(); dis_assembled_args.push_back(out_high_ptr); dis_assembled_args.push_back(out_low_ptr); diff --git a/cpp/src/gandiva/decimal_ir.h b/cpp/src/gandiva/decimal_ir.h index b11730f1e231..83f512b32f48 100644 --- a/cpp/src/gandiva/decimal_ir.h +++ b/cpp/src/gandiva/decimal_ir.h @@ -19,6 +19,7 @@ #include #include +#include #include #include "gandiva/function_ir_builder.h" @@ -137,6 +138,9 @@ class DecimalIR : public FunctionIRBuilder { ValueWithOverflow AddWithOverflowCheck(const ValueFull& x, const ValueFull& y, const ValueFull& out); + // Allocate decimal return slots in the current function's entry block. + std::pair CreateEntryBlockReturnSlots(); + // Do addition of large integers (both positive and negative). llvm::Value* AddLarge(const ValueFull& x, const ValueFull& y, const ValueFull& out);