diff --git a/CMakeLists.txt b/CMakeLists.txt index d1c4ea9..9b9cbdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,6 +201,22 @@ if(XYZ_PROTOCOL_IS_NOT_SUBPROJECT) FILES tutorials/3_reflection.cc) target_compile_options(reflection_tutorial PRIVATE -freflection) + + # Unit tests for the reflection backend's own detail headers + # (protocol_reflection_detail/*.h), independent of any interface or + # xyz::protocol machinery. + xyz_add_test( + NAME + protocol_reflection_detail_test + VERSION + 26 + FILES + protocol_reflection_detail/naming.hxx + protocol_reflection_detail/naming_test.cc) + target_compile_options(protocol_reflection_detail_test + PRIVATE -freflection) + target_include_directories(protocol_reflection_detail_test + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) endif() add_executable(protocol_benchmark protocol_benchmark.cc) diff --git a/GEMINI.md b/GEMINI.md index 6b4f6e2..c18799a 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -12,8 +12,10 @@ general defaults for this repository. names. Use descriptive names like `XYZ_GENERATE_MANUAL_VTABLE` instead of `XYZ_GEN_MAN_VT`. - **Stability:** Ensure that generated symbols (e.g., vtable entry names) remain - deterministic and stable between runs by using MD5 hashing of function - signatures. + deterministic, stable between runs, and free of collisions between distinct + signatures. See `scripts/generate_protocol.py` and + `protocol_reflection_detail/naming.hxx` for each backend's approach; no + specific mangling scheme is mandated. - **WG21 Style:** `DRAFT.md` must adhere to ISO C++ standardization proposal norms - **Paper Format:** We use pure Markdown, no YAML frontmatter, and no HTML blocks. diff --git a/protocol_reflection_detail/naming.hxx b/protocol_reflection_detail/naming.hxx new file mode 100644 index 0000000..c534c27 --- /dev/null +++ b/protocol_reflection_detail/naming.hxx @@ -0,0 +1,68 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +// Deterministic, collision-proof names for generated vtable/forwarder +// entries. Every byte outside [a-zA-Z0-9], including a literal underscore, +// becomes an underscore plus two lowercase hex digits. This is a bijection +// on byte sequences, so distinct inputs always produce distinct +// identifiers. +#ifndef XYZ_PROTOCOL_REFLECTION_DETAIL_NAMING_HXX_ +#define XYZ_PROTOCOL_REFLECTION_DETAIL_NAMING_HXX_ + +#include +#include +#include + +namespace xyz::reflection_detail { + +// `identifier_safe_string` must run at compile time. +// C++ library utilities like `std::isalnum` are not constexpr functions so we +// hand-roll our own. +// +// The 8-bit character for non-alphanumeric characters is +// split into 2 4-bit values, each of which is mapped to an alphanumeric +// character. We prefix this character pair with `_` to give each +// non-alphanumeric character a unique representation. +constexpr std::string identifier_safe_string(std::string_view s) { + constexpr std::string_view hex_digits = "0123456789abcdef"; + std::string result; + result.reserve(s.size()); + for (unsigned char c : s) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9')) { + result += static_cast(c); + } else { + result += '_'; + result += hex_digits[c >> 4]; + result += hex_digits[c & 0xf]; + } + } + return result; +} + +// The generated vtable-entry name for a reflected member function or data +// member, found by name. Operators, which have no identifier_of, need +// separate handling. +consteval std::string vtable_entry_name(std::meta::info member) { + return identifier_safe_string(std::meta::identifier_of(member)); +} + +} // namespace xyz::reflection_detail + +#endif // XYZ_PROTOCOL_REFLECTION_DETAIL_NAMING_HXX_ diff --git a/protocol_reflection_detail/naming_test.cc b/protocol_reflection_detail/naming_test.cc new file mode 100644 index 0000000..ba3c0d0 --- /dev/null +++ b/protocol_reflection_detail/naming_test.cc @@ -0,0 +1,86 @@ +/* Copyright (c) 2025 The XYZ Protocol Authors. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +==============================================================================*/ +#include "protocol_reflection_detail/naming.hxx" + +#include + +#include +#include +#include + +namespace { + +using xyz::reflection_detail::identifier_safe_string; +using xyz::reflection_detail::vtable_entry_name; + +TEST(IdentifierSafeString, AlphanumericPassesThroughUnchanged) { + EXPECT_EQ(identifier_safe_string("abcXYZ123"), "abcXYZ123"); +} + +TEST(IdentifierSafeString, EmptyStringStaysEmpty) { + EXPECT_EQ(identifier_safe_string(""), ""); +} + +TEST(IdentifierSafeString, NonAlphanumericByteBecomesUnderscoreHexPair) { + // '(' is 0x28. + EXPECT_EQ(identifier_safe_string("a(b"), "a_28b"); +} + +TEST(IdentifierSafeString, LiteralUnderscoreIsEscapedToo) { + // '_' is 0x5f: the escape marker itself is never left unescaped, so a + // genuine underscore in the input can never be confused with one half of + // an escape sequence. + EXPECT_EQ(identifier_safe_string("a_b"), "a_5fb"); +} + +TEST(IdentifierSafeString, DistinctInputsStayDistinctEvenWhenNaivelyAmbiguous) { + // A scheme that replaced every non-identifier character with a single + // "_" would map both of these to "a_b", losing the distinction. This + // scheme keeps every input distinguishable. + EXPECT_NE(identifier_safe_string("a(b"), identifier_safe_string("a)b")); + EXPECT_NE(identifier_safe_string("a(b"), identifier_safe_string("a_b")); +} + +TEST(IdentifierSafeString, ResultOnlyContainsValidIdentifierCharacters) { + std::string result = identifier_safe_string("write(int, double)::operator+"); + for (char c : result) { + EXPECT_TRUE((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_') + << "unexpected character '" << c << "' in " << result; + } +} + +struct Fixture { + int value() const { return 0; } + + void set_value(int) {} +}; + +TEST(VtableEntryName, NamesAnOrdinaryMemberByItsIdentifier) { + constexpr const char* value_name = + std::define_static_string(vtable_entry_name(^^Fixture::value)); + constexpr const char* set_value_name = + std::define_static_string(vtable_entry_name(^^Fixture::set_value)); + + EXPECT_STREQ(value_name, "value"); + EXPECT_STREQ(set_value_name, "set_5fvalue"); +} + +} // namespace