blob: e0a696e54ee7a11d50de072a3a0ec9d7af0bf36d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 <gtest/gtest.h>
#include <tvm/ffi/any.h>
#include <tvm/ffi/container/array.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/memory.h>
#include <tvm/ffi/optional.h>
#include <cstdint>
#include <cstring>
#include "./testing_object.h"
namespace {
using namespace tvm::ffi;
using namespace tvm::ffi::testing;
// Regression guard: TypeTraits<Optional<T>>::storage_enabled must PROPAGATE from
// TypeTraits<T>::storage_enabled (pass-through), not be hardcoded. This keeps a
// nested Optional<Optional<T>> and Optional<T> used inside Variant<...>/containers
// Any-backed exactly when T itself is storage-enabled.
static_assert(TypeTraits<Optional<int>>::storage_enabled == TypeTraits<int>::storage_enabled);
static_assert(TypeTraits<Optional<int>>::storage_enabled, "int is storage-enabled");
static_assert(TypeTraits<Optional<TInt>>::storage_enabled == TypeTraits<TInt>::storage_enabled);
static_assert(TypeTraits<Optional<TInt>>::storage_enabled, "ObjectRef is storage-enabled");
// A non-owning view type is NOT storage-enabled; the pass-through must carry that.
static_assert(!TypeTraits<TensorView>::storage_enabled);
static_assert(TypeTraits<Optional<TensorView>>::storage_enabled ==
TypeTraits<TensorView>::storage_enabled);
static_assert(!TypeTraits<Optional<TensorView>>::storage_enabled,
"Optional<view> must not be storage-enabled");
// Because Optional<int>/Optional<TInt> are storage-enabled, the outer
// Optional<Optional<T>> uses the Any-backed representation (sizeof == sizeof(Any)),
// and Optional<T> is accepted as a storage type (e.g. inside Variant/containers).
static_assert(sizeof(Optional<Optional<int>>) == sizeof(Any));
static_assert(sizeof(Optional<Optional<TInt>>) == sizeof(Any));
static_assert(details::storage_enabled_v<Optional<int>>);
TEST(Optional, StorageEnabledPassThrough) {
// Optional<int> is storage-enabled, so a nested Optional round-trips through
// Any via the Any-backed path.
Optional<Optional<int>> nested = Optional<int>(7);
Any any = nested;
auto back = any.cast<Optional<Optional<int>>>();
EXPECT_TRUE(back.has_value());
EXPECT_EQ(back.value().value(), 7);
}
TEST(Optional, TInt) {
Optional<TInt> x;
Optional<TInt> y = TInt(11);
// Optional<T> is uniformly backed by a single Any (TVMFFIAny) regardless of T.
static_assert(sizeof(Optional<TInt>) == sizeof(Any));
EXPECT_TRUE(!x.has_value());
EXPECT_EQ(x.value_or(TInt(12))->value, 12);
EXPECT_TRUE(y.has_value());
EXPECT_EQ(y.value_or(TInt(12))->value, 11);
Any z_any = std::move(y);
EXPECT_TRUE(z_any != nullptr);
EXPECT_EQ((z_any.cast<TInt>())->value, 11);
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_TRUE(!y.has_value());
// move from any to optional
auto y2 = std::move(z_any).cast<Optional<TInt>>();
EXPECT_TRUE(y2.has_value());
EXPECT_EQ(y2.value_or(TInt(12))->value, 11);
}
TEST(Optional, double) {
Optional<double> x;
Optional<double> y = 11.0;
// Layout is independent of the contained type: sizeof(Optional<T>) == sizeof(Any).
static_assert(sizeof(Optional<double>) == sizeof(Any));
EXPECT_TRUE(!x.has_value());
EXPECT_EQ(x.value_or(12), 12);
EXPECT_TRUE(x != 12);
EXPECT_TRUE(y.has_value());
EXPECT_EQ(y.value_or(12), 11);
EXPECT_TRUE(y == 11);
EXPECT_TRUE(y != 12);
}
TEST(Optional, AnyConvertInt) {
Optional<int> opt_v0 = 1;
EXPECT_EQ(opt_v0.value(), 1);
EXPECT_TRUE(opt_v0.has_value());
AnyView view0 = opt_v0;
EXPECT_EQ(view0.cast<int>(), 1);
Any any1;
auto opt_v1 = std::move(any1).cast<Optional<int>>();
EXPECT_TRUE(!opt_v1.has_value());
Optional<int> opt_v2 = 11;
Any any2 = std::move(opt_v2);
EXPECT_EQ(any2.cast<int>(), 11);
}
TEST(Optional, AnyConvertArray) {
AnyView view0;
Array<Array<TNumber>> arr_nested = {{}, {TInt(1), TFloat(2)}};
view0 = arr_nested;
auto opt_arr = view0.cast<Optional<Array<Array<TNumber>>>>();
EXPECT_EQ(arr_nested.use_count(), 2);
auto arr1 = view0.cast<Optional<Array<Array<TNumber>>>>();
EXPECT_EQ(arr_nested.use_count(), 3);
EXPECT_EQ(arr1.value()[1][1].as<TFloatObj>()->value, 2);
Any any1;
auto arr2 = any1.cast<Optional<Array<Array<TNumber>>>>();
EXPECT_TRUE(!arr2.has_value());
EXPECT_THROW(
{
try {
[[maybe_unused]] auto arr2 = view0.cast<Optional<Array<Array<int>>>>();
} catch (const Error& error) {
EXPECT_EQ(error.kind(), "TypeError");
std::string what = error.what();
std::cout << what << std::endl;
EXPECT_NE(what.find("to `Optional<Array<Array<int>>>`"), std::string::npos);
throw;
}
},
::tvm::ffi::Error);
}
TEST(Optional, OptionalOfOptional) {
// testcase of optional<optional>
//
// Because nullopt is uniformly represented as kTVMFFINone in the single
// TVMFFIAny backing, an engaged outer Optional that holds an empty inner
// Optional collapses to nullopt (the two states share the kTVMFFINone bit
// pattern). This matches the behavior of round-tripping a nested Optional
// through Any. Engaged-outer/engaged-inner still nests correctly.
Optional<Optional<int>> opt_opt_int;
EXPECT_TRUE(!opt_opt_int.has_value());
// engaged outer holding an empty inner collapses to nullopt.
Optional<Optional<int>> opt_opt_int2 = Optional<int>(std::nullopt);
EXPECT_TRUE(!opt_opt_int2.has_value());
// engaged outer holding an engaged inner nests correctly.
Optional<Optional<int>> opt_opt_int3 = Optional<int>(7);
EXPECT_TRUE(opt_opt_int3.has_value());
EXPECT_TRUE(opt_opt_int3.value().has_value());
EXPECT_EQ(opt_opt_int3.value().value(), 7);
// Optional<Optional<ObjectRef>>
Optional<Optional<TInt>> opt_opt_tint;
EXPECT_TRUE(!opt_opt_tint.has_value());
Optional<Optional<TInt>> opt_opt_tint2 = Optional<TInt>(std::nullopt);
EXPECT_TRUE(!opt_opt_tint2.has_value());
opt_opt_tint2 = std::nullopt;
EXPECT_TRUE(!opt_opt_tint2.has_value());
Optional<Optional<TInt>> opt_opt_tint3 = Optional<TInt>(TInt(42));
EXPECT_TRUE(opt_opt_tint3.has_value());
EXPECT_TRUE(opt_opt_tint3.value().has_value());
EXPECT_EQ(opt_opt_tint3.value().value()->value, 42);
}
TEST(Optional, ValueMove) {
Optional<TInt> y = TInt(11);
TInt x = std::move(y).value();
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_TRUE(!y.has_value());
EXPECT_EQ(x->value, 11);
Optional<TInt> opt_tint = TInt(21);
EXPECT_TRUE(opt_tint.has_value());
EXPECT_EQ((*opt_tint)->value, 21);
TInt moved_tint = *std::move(opt_tint);
EXPECT_EQ(moved_tint->value, 21);
// NOLINTNEXTLINE(bugprone-use-after-move,clang-analyzer-cplusplus.Move)
EXPECT_TRUE(!opt_tint.has_value());
}
TEST(Optional, OptionalInArray) {
// This pattern plus iteration may cause memory leak
// this is because arr[0] returns a temporary object
// and further call arr[0].value() may return a reference to
// the temporary object
Array<Optional<Array<TInt>>> arr = {Array<TInt>({TInt(0), TInt(1)})};
int counter = 0;
for (const auto& x : arr[0].value()) {
EXPECT_EQ(x->value, counter++);
}
Any any = arr;
auto opt_arr = any.cast<Array<Optional<Array<TInt>>>>();
EXPECT_EQ(opt_arr[0].value()[0]->value, 0);
}
TEST(Optional, String) {
Optional<String> opt_str;
EXPECT_TRUE(!opt_str.has_value());
EXPECT_EQ(opt_str.value_or("default"), "default");
EXPECT_TRUE(opt_str != "default");
EXPECT_TRUE(opt_str != String("default"));
EXPECT_TRUE(opt_str == std::nullopt);
opt_str = "hello";
EXPECT_TRUE(opt_str.has_value());
EXPECT_EQ(opt_str.value(), "hello");
EXPECT_TRUE(opt_str == "hello");
EXPECT_TRUE(opt_str == String("hello"));
EXPECT_TRUE(opt_str != std::nullopt);
static_assert(sizeof(Optional<String>) == sizeof(Any));
}
TEST(Optional, Bytes) {
Optional<Bytes> opt_bytes;
EXPECT_TRUE(!opt_bytes.has_value());
EXPECT_EQ(opt_bytes.value_or(std::string("default")), "default");
opt_bytes = std::string("hello");
EXPECT_TRUE(opt_bytes.has_value());
EXPECT_EQ(opt_bytes.value().operator std::string(), "hello");
EXPECT_TRUE(opt_bytes != std::nullopt);
static_assert(sizeof(Optional<Bytes>) == sizeof(Any));
}
// Optional<T> is uniformly backed by a single TVMFFIAny (Any), so its layout is
// independent of the contained type: every Optional<T> has the size and
// alignment of a TVMFFIAny. (The Rust binding's in-place Optional<T> mirror is a
// separate follow-up that adopts this uniform 16-byte representation.)
template <typename... T>
constexpr bool all_optional_layouts_uniform_v =
((sizeof(Optional<T>) == sizeof(TVMFFIAny) && alignof(Optional<T>) == alignof(TVMFFIAny)) &&
...);
static_assert(
all_optional_layouts_uniform_v<bool, int8_t, int16_t, int32_t, int64_t, uint8_t, uint16_t,
uint32_t, uint64_t, float, double, String, Bytes>);
} // namespace