diff --git a/velox/expression/fuzzer/ArgGenerator.h b/velox/expression/fuzzer/ArgGenerator.h new file mode 100644 index 000000000000..4c015f7a38c7 --- /dev/null +++ b/velox/expression/fuzzer/ArgGenerator.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "velox/expression/FunctionSignature.h" +#include "velox/vector/fuzzer/Utils.h" + +namespace facebook::velox::fuzzer { + +/// Generates random, but valid input types for a specified function signature +/// with the return type. +class ArgGenerator { + public: + virtual ~ArgGenerator() = default; + + /// Given a signature and a concrete return type returns randomly selected + /// valid input types. Returns empty vector if no input types can produce the + /// specified result type. + virtual std::vector generateArgs( + const exec::FunctionSignature& signature, + const TypePtr& returnType, + FuzzerGenerator& rng) = 0; +}; + +} // namespace facebook::velox::fuzzer diff --git a/velox/expression/fuzzer/CMakeLists.txt b/velox/expression/fuzzer/CMakeLists.txt index ca961f58e50e..adb2949e7b40 100644 --- a/velox/expression/fuzzer/CMakeLists.txt +++ b/velox/expression/fuzzer/CMakeLists.txt @@ -19,8 +19,9 @@ target_link_libraries(velox_expression_test_utility velox_type velox_expression_functions gtest) add_library( - velox_expression_fuzzer ArgumentTypeFuzzer.cpp ExpressionFuzzer.cpp - FuzzerRunner.cpp ExpressionFuzzerVerifier.cpp) + velox_expression_fuzzer + ArgumentTypeFuzzer.cpp DecimalArgGeneratorBase.cpp ExpressionFuzzer.cpp + FuzzerRunner.cpp ExpressionFuzzerVerifier.cpp) target_link_libraries( velox_expression_fuzzer diff --git a/velox/expression/fuzzer/DecimalArgGeneratorBase.cpp b/velox/expression/fuzzer/DecimalArgGeneratorBase.cpp new file mode 100644 index 000000000000..1b65b67658c5 --- /dev/null +++ b/velox/expression/fuzzer/DecimalArgGeneratorBase.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "velox/expression/fuzzer/DecimalArgGeneratorBase.h" +#include + +namespace facebook::velox::fuzzer { +namespace { + +// Returns all the possible decimal types. +const std::vector& getAllTypes() { + const auto generateAllTypes = []() { + std::vector allTypes; + for (auto p = 1; p <= 38; ++p) { + for (auto s = 0; s <= p; ++s) { + allTypes.push_back(DECIMAL(p, s)); + } + } + return allTypes; + }; + + static const std::vector allTypes = generateAllTypes(); + return allTypes; +} + +uint32_t rand32(uint32_t max, FuzzerGenerator& rng) { + return boost::random::uniform_int_distribution()(rng) % max; +} +} // namespace + +std::vector DecimalArgGeneratorBase::generateArgs( + const exec::FunctionSignature& /*signature*/, + const TypePtr& returnType, + FuzzerGenerator& rng) { + auto inputs = findInputs(returnType, rng); + for (const auto& input : inputs) { + if (input == nullptr) { + return {}; + } + } + return inputs; +} + +void DecimalArgGeneratorBase::initialize(uint32_t numArgs) { + switch (numArgs) { + case 1: { + for (const auto& t : getAllTypes()) { + auto [p, s] = getDecimalPrecisionScale(*t); + if (auto returnType = toReturnType(p, s)) { + inputs_[returnType.value()].push_back({t}); + } + } + break; + } + case 2: { + for (const auto& a : getAllTypes()) { + for (const auto& b : getAllTypes()) { + auto [p1, s1] = getDecimalPrecisionScale(*a); + auto [p2, s2] = getDecimalPrecisionScale(*b); + + if (auto returnType = toReturnType(p1, s1, p2, s2)) { + inputs_[returnType.value()].push_back({a, b}); + } + } + } + break; + } + default: + VELOX_NYI( + "Initialization with {} argument types is not supported.", numArgs); + } +} + +std::vector DecimalArgGeneratorBase::findInputs( + const TypePtr& returnType, + FuzzerGenerator& rng) const { + const auto [p, s] = getDecimalPrecisionScale(*returnType); + const auto it = inputs_.find({p, s}); + if (it == inputs_.end()) { + VLOG(1) << "Cannot find input types for " << returnType->toString(); + return {}; + } + + const auto index = rand32(it->second.size(), rng); + return it->second[index]; +} +} // namespace facebook::velox::fuzzer diff --git a/velox/expression/fuzzer/DecimalArgGeneratorBase.h b/velox/expression/fuzzer/DecimalArgGeneratorBase.h new file mode 100644 index 000000000000..c27db1b8d264 --- /dev/null +++ b/velox/expression/fuzzer/DecimalArgGeneratorBase.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "velox/expression/fuzzer/ArgGenerator.h" + +namespace facebook::velox::fuzzer { + +/// An argument type generator for decimal function. A map keyed on the pair of +/// precision and scale could be initialized with all possible input types. +/// Argument types are generated by looking up the map with the precision and +/// scale of return type, and randomly selecting valid input types. Derived +/// classes should call 'initialize' from the constructor and specify the number +/// of decimal arguments. They should also implement toReturnType with matching +/// number of pairs of precision and scale. +class DecimalArgGeneratorBase : public ArgGenerator { + public: + std::vector generateArgs( + const exec::FunctionSignature& signature, + const TypePtr& returnType, + FuzzerGenerator& rng) override; + + protected: + // Computes result type for all possible pairs of decimal input types. Stores + // the results in 'inputs_' map keyed by the precision and scale of return + // type. + // @param numArgs the number of decimal argument types. It only supports + // initialization with one or two argument types. + virtual void initialize(uint32_t numArgs); + + // Given precisions and scales of the inputs, returns precision and scale of + // the result. Returns std::nullopt if a valid return type cannot be generated + // with inputs. Used when the return type is generated with one pair of input + // precision and scale. + virtual std::optional> toReturnType(int p, int s) { + VELOX_UNREACHABLE(); + } + + // Used when the return type is generated with two pairs of input precision + // and scale. + virtual std::optional> + toReturnType(int p1, int s1, int p2, int s2) { + VELOX_UNREACHABLE(); + } + + private: + // Returns randomly selected pair of input types that produce the specified + // result type. + std::vector findInputs( + const TypePtr& returnType, + FuzzerGenerator& rng) const; + + // Maps from the precision and scale of return type to corresponding input + // types. + std::unordered_map, std::vector>> + inputs_; +}; + +} // namespace facebook::velox::fuzzer diff --git a/velox/expression/fuzzer/tests/CMakeLists.txt b/velox/expression/fuzzer/tests/CMakeLists.txt index 459189ed4c80..64af95929dcb 100644 --- a/velox/expression/fuzzer/tests/CMakeLists.txt +++ b/velox/expression/fuzzer/tests/CMakeLists.txt @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -add_executable(velox_expression_fuzzer_unit_test ArgumentTypeFuzzerTest.cpp ExpressionFuzzerUnitTest.cpp) +add_executable(velox_expression_fuzzer_unit_test ArgumentTypeFuzzerTest.cpp DecimalArgGeneratorTest.cpp ExpressionFuzzerUnitTest.cpp) target_link_libraries( velox_expression_fuzzer_unit_test diff --git a/velox/expression/fuzzer/tests/DecimalArgGeneratorTest.cpp b/velox/expression/fuzzer/tests/DecimalArgGeneratorTest.cpp new file mode 100644 index 000000000000..4de9c473ad01 --- /dev/null +++ b/velox/expression/fuzzer/tests/DecimalArgGeneratorTest.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "velox/expression/SignatureBinder.h" +#include "velox/expression/fuzzer/DecimalArgGeneratorBase.h" + +namespace facebook::velox::fuzzer::test { + +class DecimalArgGeneratorTest : public testing::Test { + protected: + class UnaryArgGenerator : public DecimalArgGeneratorBase { + public: + UnaryArgGenerator() { + initialize(1); + } + + protected: + std::optional> toReturnType(int p, int s) override { + auto precision = std::min(38, p + s + 1); + auto scale = std::min(s + 1, 18); + return {{precision, scale}}; + } + }; + + class BinaryArgGenerator : public DecimalArgGeneratorBase { + public: + BinaryArgGenerator() { + initialize(2); + } + + protected: + std::optional> + toReturnType(int p1, int s1, int p2, int s2) override { + auto s = std::max(s1, s2); + auto p = std::min(38, std::max(p1 - s1, p2 - s2) + std::max(s1, s2) + 1); + return {{p, s}}; + } + }; + + // Assert the equivalence between the given return type and the actual type + // resolved from generated argument types. + void assertReturnType( + const std::shared_ptr& generator, + const exec::FunctionSignature& signature, + const TypePtr& returnType) { + std::mt19937 seed{0}; + const auto argTypes = generator->generateArgs(signature, returnType, seed); + + // Resolve return type from argument types for the given signature. + TypePtr actualType; + exec::SignatureBinder binder(signature, argTypes); + if (binder.tryBind()) { + actualType = binder.tryResolveReturnType(); + } else { + VELOX_FAIL("Failed to resolve return type from argument types."); + } + EXPECT_TRUE(returnType->equivalent(*actualType)) + << "Expected type: " << returnType->toString() + << ", actual type: " << actualType->toString(); + } + + // Assert that no argument types can be generated for the given return type. + void assertEmptyArgs( + std::shared_ptr generator, + const exec::FunctionSignature& signature, + const TypePtr& returnType) { + std::mt19937 seed{0}; + const auto argTypes = generator->generateArgs(signature, returnType, seed); + EXPECT_TRUE(argTypes.empty()); + } +}; + +TEST_F(DecimalArgGeneratorTest, unary) { + auto signature = + exec::FunctionSignatureBuilder() + .integerVariable("scale") + .integerVariable("precision") + .integerVariable("r_precision", "min(38, precision + scale + 1)") + .integerVariable("r_scale", "min(scale + 1, 18)") + .returnType("decimal(r_precision, r_scale)") + .argumentType("decimal(precision, scale)") + .build(); + + const auto generator = std::make_shared(); + for (auto returnType : {DECIMAL(10, 2), DECIMAL(38, 18)}) { + assertReturnType(generator, *signature, returnType); + } + assertEmptyArgs(generator, *signature, DECIMAL(38, 20)); +} + +TEST_F(DecimalArgGeneratorTest, binary) { + auto signature = + exec::FunctionSignatureBuilder() + .integerVariable("a_scale") + .integerVariable("b_scale") + .integerVariable("a_precision") + .integerVariable("b_precision") + .integerVariable( + "r_precision", + "min(38, max(a_precision - a_scale, b_precision - b_scale) + max(a_scale, b_scale) + 1)") + .integerVariable("r_scale", "max(a_scale, b_scale)") + .returnType("decimal(r_precision, r_scale)") + .argumentType("decimal(a_precision, a_scale)") + .argumentType("decimal(b_precision, b_scale)") + .build(); + + const auto generator = std::make_shared(); + for (auto returnType : + {DECIMAL(10, 2), DECIMAL(38, 20), DECIMAL(38, 38), DECIMAL(38, 0)}) { + assertReturnType(generator, *signature, returnType); + } +} + +} // namespace facebook::velox::fuzzer::test