From 0317007ee10bed8649af872a618a20be960bb9c9 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 14 Apr 2026 18:58:46 +0200 Subject: [PATCH] AK: Make short ASCII string literals compile-time constants Make the _string, _fly_string, _utf16, and _utf16_fly_string UDL operators constexpr, with a fast path for short (<= 7 byte) ASCII literals that folds directly into an inline ShortString. Previously, every "foo"_fly_string (and friends) involved an out-of-line call into the string factory, even though the result is entirely known at compile time. --- AK/FlyString.h | 19 +++++++++++++++++- AK/String.h | 18 ++++++++++++++++- AK/Utf16FlyString.h | 35 ++++++++++++++++++++++++++++++--- AK/Utf16String.h | 34 ++++++++++++++++++++++++++++++-- Tests/AK/TestFlyString.cpp | 8 ++++++++ Tests/AK/TestString.cpp | 8 ++++++++ Tests/AK/TestUtf16FlyString.cpp | 12 +++++++++++ Tests/AK/TestUtf16String.cpp | 12 +++++++++++ 8 files changed, 139 insertions(+), 7 deletions(-) diff --git a/AK/FlyString.h b/AK/FlyString.h index f909d396ff..36576e268b 100644 --- a/AK/FlyString.h +++ b/AK/FlyString.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -25,6 +26,16 @@ public: static ErrorOr from_utf8(StringView); static FlyString from_utf8_without_validation(ReadonlyBytes); + + [[nodiscard]] static constexpr FlyString from_ascii_short_string_without_validation(char const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return FlyString { Detail::StringBase { short_string } }; + } + template requires(IsOneOf, ByteString, FlyString, String>) static ErrorOr from_utf8(T&&) = delete; @@ -131,8 +142,14 @@ struct ASCIICaseInsensitiveFlyStringTraits : public Traits { } -[[nodiscard]] ALWAYS_INLINE AK::FlyString operator""_fly_string(char const* cstring, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::FlyString operator""_fly_string(char const* cstring, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime validation or table lookup. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(cstring, cstring + length, AK::is_ascii)) { + return AK::FlyString::from_ascii_short_string_without_validation(cstring, length); + } + ASSERT(Utf8View(AK::StringView(cstring, length)).validate()); return AK::FlyString::from_utf8_without_validation({ cstring, length }); } diff --git a/AK/String.h b/AK/String.h index f37805c1b2..4e007cffc1 100644 --- a/AK/String.h +++ b/AK/String.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -56,6 +57,15 @@ public: [[nodiscard]] static String from_utf8_without_validation(ReadonlyBytes); [[nodiscard]] static String from_ascii_without_validation(ReadonlyBytes); + [[nodiscard]] static constexpr String from_ascii_short_string_without_validation(char const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return String { StringBase { short_string } }; + } + static ErrorOr from_string_builder(Badge, StringBuilder&); [[nodiscard]] static String from_string_builder_without_validation(Badge, StringBuilder&); @@ -266,8 +276,14 @@ struct ASCIICaseInsensitiveStringTraits : public Traits { } -[[nodiscard]] ALWAYS_INLINE AK::String operator""_string(char const* cstring, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::String operator""_string(char const* cstring, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime validation or heap allocation. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(cstring, cstring + length, AK::is_ascii)) { + return AK::String::from_ascii_short_string_without_validation(cstring, length); + } + ASSERT(Utf8View(AK::StringView(cstring, length)).validate()); return AK::String::from_utf8_without_validation({ cstring, length }); } diff --git a/AK/Utf16FlyString.h b/AK/Utf16FlyString.h index 5e7513b38a..535d8473af 100644 --- a/AK/Utf16FlyString.h +++ b/AK/Utf16FlyString.h @@ -146,8 +146,26 @@ public: // This is primarily interesting to unit tests. [[nodiscard]] static size_t number_of_utf16_fly_strings(); + [[nodiscard]] static constexpr Utf16FlyString from_ascii_short_string_without_validation(char const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return Utf16FlyString { Detail::Utf16StringBase { short_string } }; + } + + [[nodiscard]] static constexpr Utf16FlyString from_ascii_short_string_without_validation(char16_t const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return Utf16FlyString { Detail::Utf16StringBase { short_string } }; + } + private: - ALWAYS_INLINE explicit Utf16FlyString(Detail::Utf16StringBase data) + ALWAYS_INLINE constexpr explicit Utf16FlyString(Detail::Utf16StringBase data) : m_data(move(data)) { } @@ -202,15 +220,26 @@ inline constexpr bool IsHashCompatible = true; } -[[nodiscard]] ALWAYS_INLINE AK::Utf16FlyString operator""_utf16_fly_string(char const* string, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::Utf16FlyString operator""_utf16_fly_string(char const* string, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime validation or table lookup. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(string, string + length, AK::is_ascii)) { + return AK::Utf16FlyString::from_ascii_short_string_without_validation(string, length); + } + AK::StringView view { string, length }; ASSERT(AK::Utf8View { view }.validate()); return AK::Utf16FlyString::from_utf8_without_validation(view); } -[[nodiscard]] ALWAYS_INLINE AK::Utf16FlyString operator""_utf16_fly_string(char16_t const* string, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::Utf16FlyString operator""_utf16_fly_string(char16_t const* string, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime work. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(string, string + length, AK::is_ascii)) + return AK::Utf16FlyString::from_ascii_short_string_without_validation(string, length); + return AK::Utf16FlyString::from_utf16({ string, length }); } diff --git a/AK/Utf16String.h b/AK/Utf16String.h index a4250ac7ee..4bf9373337 100644 --- a/AK/Utf16String.h +++ b/AK/Utf16String.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include @@ -81,6 +82,24 @@ public: return Utf16String { short_string }; } + [[nodiscard]] static constexpr Utf16String from_ascii_short_string_without_validation(char const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return Utf16String { short_string }; + } + + [[nodiscard]] static constexpr Utf16String from_ascii_short_string_without_validation(char16_t const* data, size_t length) + { + VERIFY(length <= Detail::MAX_SHORT_STRING_BYTE_COUNT); + auto short_string = Detail::ShortString::create_with_byte_count(length); + for (size_t i = 0; i < length; ++i) + short_string.storage[i] = static_cast(data[i]); + return Utf16String { short_string }; + } + ALWAYS_INLINE static Utf16String from_code_point(u32 code_point) { Array code_units; @@ -331,15 +350,26 @@ struct Traits : public DefaultTraits { } -[[nodiscard]] ALWAYS_INLINE AK::Utf16String operator""_utf16(char const* string, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::Utf16String operator""_utf16(char const* string, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime validation or heap allocation. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(string, string + length, AK::is_ascii)) { + return AK::Utf16String::from_ascii_short_string_without_validation(string, length); + } + AK::StringView view { string, length }; ASSERT(AK::Utf8View { view }.validate()); return AK::Utf16String::from_utf8_without_validation(view); } -[[nodiscard]] ALWAYS_INLINE AK::Utf16String operator""_utf16(char16_t const* string, size_t length) +[[nodiscard]] ALWAYS_INLINE constexpr AK::Utf16String operator""_utf16(char16_t const* string, size_t length) { + // OPTIMIZATION: Short ASCII strings become compile-time constants with no runtime work. + if (length <= AK::Detail::MAX_SHORT_STRING_BYTE_COUNT + && AK::all_of(string, string + length, AK::is_ascii)) + return AK::Utf16String::from_ascii_short_string_without_validation(string, length); + return AK::Utf16String::from_utf16({ string, length }); } diff --git a/Tests/AK/TestFlyString.cpp b/Tests/AK/TestFlyString.cpp index 6f1e0d6b5d..58f1bbf62b 100644 --- a/Tests/AK/TestFlyString.cpp +++ b/Tests/AK/TestFlyString.cpp @@ -20,6 +20,14 @@ TEST_CASE(empty_string) EXPECT_EQ(FlyString::number_of_fly_strings(), 0u); } +TEST_CASE(short_ascii_literal_is_constexpr) +{ + // The _fly_string UDL folds short ASCII literals to a compile-time constant. + static constexpr FlyString fly = "foo"_fly_string; + EXPECT_EQ(fly, "foo"sv); + EXPECT_EQ(fly.bytes_as_string_view().length(), 3u); +} + TEST_CASE(short_string) { FlyString fly1 { "foo"_string }; diff --git a/Tests/AK/TestString.cpp b/Tests/AK/TestString.cpp index 799671a1f3..d926fc565e 100644 --- a/Tests/AK/TestString.cpp +++ b/Tests/AK/TestString.cpp @@ -18,6 +18,14 @@ #include #include +TEST_CASE(short_ascii_literal_is_constexpr) +{ + // The _string UDL folds short ASCII literals to a compile-time constant. + static constexpr String s = "foo"_string; + EXPECT_EQ(s, "foo"sv); + EXPECT_EQ(s.bytes().size(), 3u); +} + TEST_CASE(construct_empty) { String empty; diff --git a/Tests/AK/TestUtf16FlyString.cpp b/Tests/AK/TestUtf16FlyString.cpp index 1f7d6ccb8d..9dfcb7184e 100644 --- a/Tests/AK/TestUtf16FlyString.cpp +++ b/Tests/AK/TestUtf16FlyString.cpp @@ -14,6 +14,18 @@ static_assert(AK::Concepts::HashCompatible); static_assert(AK::Concepts::HashCompatible); static_assert(AK::Concepts::HashCompatible); +TEST_CASE(short_ascii_literal_is_constexpr) +{ + // The _utf16_fly_string UDL folds short ASCII literals to compile-time + // constants, both for char and char16_t inputs. + static constexpr Utf16FlyString from_char = "foo"_utf16_fly_string; + static constexpr Utf16FlyString from_char16 = u"bar"_utf16_fly_string; + EXPECT_EQ(from_char, "foo"sv); + EXPECT_EQ(from_char16, "bar"sv); + EXPECT_EQ(from_char.length_in_code_units(), 3u); + EXPECT_EQ(from_char16.length_in_code_units(), 3u); +} + TEST_CASE(empty_string) { Utf16FlyString fly {}; diff --git a/Tests/AK/TestUtf16String.cpp b/Tests/AK/TestUtf16String.cpp index 66e1cdcd59..5368a96f0e 100644 --- a/Tests/AK/TestUtf16String.cpp +++ b/Tests/AK/TestUtf16String.cpp @@ -24,6 +24,18 @@ static Utf16String make_copy(Utf16String const& string) : Utf16String::from_utf16(string.utf16_view()); } +TEST_CASE(short_ascii_literal_is_constexpr) +{ + // The _utf16 UDL folds short ASCII literals to compile-time constants, + // both for char and char16_t inputs. + static constexpr Utf16String from_char = "foo"_utf16; + static constexpr Utf16String from_char16 = u"bar"_utf16; + EXPECT_EQ(from_char, "foo"sv); + EXPECT_EQ(from_char16, "bar"sv); + EXPECT_EQ(from_char.length_in_code_units(), 3u); + EXPECT_EQ(from_char16.length_in_code_units(), 3u); +} + TEST_CASE(empty_string) { Utf16String string {};