Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] Implement a universal printer #51151

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

dentiny
Copy link
Contributor

@dentiny dentiny commented Mar 7, 2025

Learned from my prev company how to use ordered visitor to implement a universal printer, which could handle ANY type.

Checkout unit tests:

enum class MyEnum { kEnum };
struct Point {
template <typename Sink>
friend void AbslStringify(Sink &sink, const Point &p) {
absl::Format(&sink, "(%d, %d)", p.x, p.y);
}
int x;
int y;
};
struct DebugStringStruct {
std::string DebugString() const { return "hello"; }
};
} // namespace
TEST(DebugStringTest, LiteralTest) {
// Literal.
EXPECT_EQ(DebugString(1), "1");
// Debug string.
EXPECT_EQ(DebugString(DebugStringStruct{}), "hello");
// Container.
const std::vector<int> vec{1, 2, 3};
EXPECT_EQ(DebugString(vec), "[1, 2, 3]");
// std::byte.
const std::byte b{10};
EXPECT_EQ(DebugString(b), "b[0x0a]");
// Boolean.
EXPECT_EQ(DebugString(true), "true");
// nullptr
EXPECT_EQ(DebugString(nullptr), "nullptr");
// Tuple.
const std::tuple<int, std::string, double> tpl{4, "hello", 4.5};
EXPECT_EQ(DebugString(tpl), "[4, hello, 4.5]");
// Map.
std::map<std::string, std::string> m;
m.emplace("a", "b");
m.emplace("hello", "world");
EXPECT_EQ(DebugString(m), "[{a, b}, {hello, world}]");
// Pair.
std::pair<int, double> p;
p.first = 5;
p.second = 10.6;
EXPECT_EQ(DebugString(p), "{5, 10.6}");
// Container inside of container.
const std::vector<std::vector<int>> cont{
{1, 2, 3},
{4, 5, 6},
};
EXPECT_EQ(DebugString(cont), "[[1, 2, 3], [4, 5, 6]]");
// Enum.
EXPECT_EQ(DebugString(MyEnum::kEnum), "0");
// Abseil stringify.
EXPECT_EQ(DebugString(Point{.x = 10, .y = 20}), "(10, 20)");
// std::optional
const std::optional<int> has_value{10};
const std::optional<int> no_value = std::nullopt;
EXPECT_EQ(DebugString(has_value), "10");
EXPECT_EQ(DebugString(no_value), "(nullopt)");
// std::optional with std::vector and std::map inside.
std::map<std::string, int> uno_m = {
{"hello", 6},
};
std::vector<std::map<std::string, int>> map_vec = {uno_m};
std::optional<std::vector<std::map<std::string, int>>> opt_vec_map = map_vec;
EXPECT_EQ(DebugString(opt_vec_map), "[[{hello, 6}]]");
// std::type_info
EXPECT_EQ(DebugString(typeid(Point)), "ray::(anonymous namespace)::Point");
// std::variant
std::variant<std::monostate, std::string, double, std::vector<std::vector<int>>> v;
v = std::monostate{};
EXPECT_EQ(DebugString(v), "(monostate)");
v = std::string{"hello world"};
EXPECT_EQ(DebugString(v), "hello world");
v = std::vector<std::vector<int>>{
std::vector<int>{1},
std::vector<int>{2},
};
EXPECT_EQ(DebugString(v), "[[1], [2]]");
// Complex type.
std::vector<int> vec1 = {1};
std::vector<std::vector<int>> vec2 = {vec1, vec1};
std::vector<std::vector<std::vector<int>>> vec3 = {vec2, vec2};
std::vector<std::vector<std::vector<std::vector<int>>>> vec4 = {vec3, vec3};
EXPECT_EQ(DebugString(vec4), "[[[[1], [1]], [[1], [1]]], [[[1], [1]], [[1], [1]]]]");
// Complex with self-defined type.
std::vector<Point> objs{Point{.x = 5, .y = 6}};
EXPECT_EQ(DebugString(objs), "[(5, 6)]");
}

Example usage 1:
We could replace all usage of RAY_CHECK to RAY_CHECK_EQ, etc

RAY_CHECK(RAY_PREDICT_TRUE(_left_ op _right_)) << " " << _left_ << " vs " << _right_

In the past it's not feasible because current implementation assume all type ostream-printable.

Detailed example:

RAY_CHECK(op_status == boost::fibers::channel_op_status::success);

RAY_CHECK(task_status_ == rpc::TaskStatus::SUBMITTED_TO_WORKER)

RAY_CHECK(task_status_ == rpc::TaskStatus::SUBMITTED_TO_WORKER)

The list goes on.

Example usage 2:
https://github.com/ray-project/ray/blob/master/src/ray/util/container_util.h

Current implementation hard codes all container type we've met, with the debug printer we no longer need it.

@dentiny dentiny requested review from israbbani, edoakes and dayshah March 7, 2025 06:33
@dentiny dentiny added the go add ONLY when ready to merge, run all tests label Mar 7, 2025
@dentiny dentiny force-pushed the hjiang/debug-string branch 3 times, most recently from 5f161b0 to 61646d5 Compare March 7, 2025 12:01
@edoakes
Copy link
Collaborator

edoakes commented Mar 7, 2025

This looks like more complexity & indirection than we need for string formatting, prefer to keep this simple

@dentiny
Copy link
Contributor Author

dentiny commented Mar 7, 2025

This looks like more complexity & indirection than we need for string formatting, prefer to keep this simple

Could you please tell me another way to print all values?

@dentiny
Copy link
Contributor Author

dentiny commented Mar 7, 2025

@edoakes As discussed offline, I added example usage to the PR description.

Copy link
Contributor

@dayshah dayshah left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will continue review later, in general though, have we considered a separate util repo from which we can try to upstream stuff to absl or other util repos

namespace ray {

// TODO(hjiang): Later version of abseil contains abseil string defined trait.
template <typename T, typename = void, typename = void>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the two extra void template params for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the fallback template argument (basically for SFINAE, fallback template type should match L31-L34)

SinkType,
std::void_t<decltype(AbslStringify(std::declval<SinkType &>(),
std::declval<const T &>()))>>
: std::true_type {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you write this differently with concepts, that should make this easier to read? (in perfect cpp20 world)

Copy link
Contributor Author

@dentiny dentiny Mar 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah in my prev company all in concepts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yes, if we do have C++20 I will update to concepts and modern type traits


template <typename C>
void PrintArray(std::ostream &os, const C &container) const {
static_assert(is_container_v<C>);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be good to describe more specifically your definition of "array" with like something that's indexable. also a more specific is_array_v

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_array_v is not correct, both contiguous container and non-continuous one like deque should apply

@dentiny dentiny requested a review from jjyao March 7, 2025 23:00
@dentiny dentiny requested a review from dayshah March 7, 2025 23:33
Signed-off-by: dentiny <dentinyhao@gmail.com>
@dentiny dentiny force-pushed the hjiang/debug-string branch from f08a280 to 53c7780 Compare March 8, 2025 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
go add ONLY when ready to merge, run all tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants