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.
This commit is contained in:
Andreas Kling 2026-04-14 18:58:46 +02:00 committed by Andreas Kling
parent 7c1d359790
commit 0317007ee1
8 changed files with 139 additions and 7 deletions

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/AllOf.h>
#include <AK/Error.h>
#include <AK/Format.h>
#include <AK/Optional.h>
@ -25,6 +26,16 @@ public:
static ErrorOr<FlyString> 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<u8>(data[i]);
return FlyString { Detail::StringBase { short_string } };
}
template<typename T>
requires(IsOneOf<RemoveCVReference<T>, ByteString, FlyString, String>)
static ErrorOr<String> from_utf8(T&&) = delete;
@ -131,8 +142,14 @@ struct ASCIICaseInsensitiveFlyStringTraits : public Traits<String> {
}
[[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 });
}

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/AllOf.h>
#include <AK/CharacterTypes.h>
#include <AK/Concepts.h>
#include <AK/Format.h>
@ -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<u8>(data[i]);
return String { StringBase { short_string } };
}
static ErrorOr<String> from_string_builder(Badge<StringBuilder>, StringBuilder&);
[[nodiscard]] static String from_string_builder_without_validation(Badge<StringBuilder>, StringBuilder&);
@ -266,8 +276,14 @@ struct ASCIICaseInsensitiveStringTraits : public Traits<String> {
}
[[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 });
}

View file

@ -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<u8>(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<u8>(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<Utf16FlyString, Utf16String> = 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 });
}

View file

@ -6,6 +6,7 @@
#pragma once
#include <AK/AllOf.h>
#include <AK/Badge.h>
#include <AK/Error.h>
#include <AK/Format.h>
@ -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<u8>(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<u8>(data[i]);
return Utf16String { short_string };
}
ALWAYS_INLINE static Utf16String from_code_point(u32 code_point)
{
Array<char16_t, 2> code_units;
@ -331,15 +350,26 @@ struct Traits<Utf16String> : public DefaultTraits<Utf16String> {
}
[[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 });
}

View file

@ -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 };

View file

@ -18,6 +18,14 @@
#include <AK/Vector.h>
#include <ctype.h>
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;

View file

@ -14,6 +14,18 @@ static_assert(AK::Concepts::HashCompatible<Utf16FlyString, Utf16String>);
static_assert(AK::Concepts::HashCompatible<Utf16View, Utf16FlyString>);
static_assert(AK::Concepts::HashCompatible<Utf16FlyString, Utf16View>);
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 {};

View file

@ -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 {};