Produce JS-visible string results as UTF-16 at their source, including numeric formatting, BigInt and BigFraction formatting, URI encoding, console formatting, parser errors, regular expression errors, Intl and Temporal records, LibUnicode locale boundaries, and LibWeb bindings. Handle fractional radix formatting through the UTF-16 builder view.
201 lines
6.5 KiB
C++
201 lines
6.5 KiB
C++
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/StringView.h>
|
|
#include <AK/Try.h>
|
|
#include <LibGC/Root.h>
|
|
#include <LibJS/Runtime/PrimitiveString.h>
|
|
#include <LibJS/Runtime/Realm.h>
|
|
#include <LibJS/Runtime/VM.h>
|
|
#include <LibTest/TestCase.h>
|
|
|
|
using namespace JS;
|
|
|
|
namespace {
|
|
|
|
struct TestVM {
|
|
TestVM()
|
|
: vm(VM::create())
|
|
, execution_context(MUST(Realm::initialize_host_defined_realm(*vm, nullptr, nullptr)))
|
|
{
|
|
}
|
|
|
|
~TestVM()
|
|
{
|
|
vm->pop_execution_context();
|
|
}
|
|
|
|
NonnullRefPtr<VM> vm;
|
|
NonnullOwnPtr<ExecutionContext> execution_context;
|
|
};
|
|
|
|
NEVER_INLINE static void materialize_temporary_rope(VM& vm)
|
|
{
|
|
auto rope = PrimitiveString::create(vm,
|
|
*PrimitiveString::create(vm, "hello"_utf16),
|
|
*PrimitiveString::create(vm, "world"_utf16));
|
|
EXPECT(rope->utf16_string_view() == "helloworld"sv);
|
|
}
|
|
|
|
NEVER_INLINE static void materialize_temporary_substring(VM& vm)
|
|
{
|
|
auto source = PrimitiveString::create(vm, "foobar"_utf16);
|
|
auto substring = PrimitiveString::create(vm, *source, 0, 3);
|
|
EXPECT(substring->utf16_string_view() == "foo"sv);
|
|
}
|
|
|
|
NEVER_INLINE static void clobber_stack()
|
|
{
|
|
char volatile stack_bytes[4096] {};
|
|
for (size_t i = 0; i < sizeof(stack_bytes); ++i)
|
|
stack_bytes[i] = static_cast<char>(i);
|
|
}
|
|
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_supports_nested_ranges)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "abcdef"_utf16);
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 4);
|
|
auto nested_substring = PrimitiveString::create(*test_vm.vm, *substring, 1, 2);
|
|
|
|
EXPECT_EQ(substring->length_in_utf16_code_units(), 4u);
|
|
EXPECT(substring->utf16_string_view() == "bcde"sv);
|
|
EXPECT_EQ(nested_substring->length_in_utf16_code_units(), 2u);
|
|
EXPECT(nested_substring->utf16_string_view() == "cd"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_materializes_rope_ranges)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto rope = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "abcd"_utf16),
|
|
*PrimitiveString::create(*test_vm.vm, "efgh"_utf16));
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *rope, 3, 3);
|
|
|
|
EXPECT_EQ(substring->length_in_utf16_code_units(), 3u);
|
|
EXPECT(substring->utf16_string_view() == "def"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_short_flat_strings_creates_flat_string)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "foo"_utf16),
|
|
*PrimitiveString::create(*test_vm.vm, "bar"_utf16));
|
|
|
|
EXPECT(concatenated->has_utf16_string());
|
|
EXPECT(concatenated->utf16_string_view() == "foobar"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_longer_strings_stays_deferred)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm,
|
|
*PrimitiveString::create(*test_vm.vm, "abcd"_utf16),
|
|
*PrimitiveString::create(*test_vm.vm, "efgh"_utf16));
|
|
|
|
EXPECT(!concatenated->has_utf16_string());
|
|
EXPECT(concatenated->utf16_string_view() == "abcdefgh"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_reuses_cached_single_ascii_strings)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto cached_b = PrimitiveString::create(*test_vm.vm, "b"_utf16);
|
|
auto string = PrimitiveString::create(*test_vm.vm, "abcd"_utf16);
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
|
|
EXPECT_EQ(substring.ptr(), cached_b.ptr());
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_handles_surrogate_boundaries)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "😀x"_utf16);
|
|
auto leading_surrogate = PrimitiveString::create(*test_vm.vm, *string, 0, 1);
|
|
auto trailing_surrogate = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
auto full_code_point = PrimitiveString::create(*test_vm.vm, *string, 0, 2);
|
|
|
|
EXPECT_EQ(leading_surrogate->length_in_utf16_code_units(), 1u);
|
|
EXPECT_EQ(leading_surrogate->utf16_string_view().code_unit_at(0), static_cast<u16>(0xd83d));
|
|
EXPECT_EQ(trailing_surrogate->length_in_utf16_code_units(), 1u);
|
|
EXPECT_EQ(trailing_surrogate->utf16_string_view().code_unit_at(0), static_cast<u16>(0xde00));
|
|
EXPECT(full_code_point->utf16_string_view() == "😀"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_concat_short_strings_handles_surrogate_boundaries)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, "😀"_utf16);
|
|
auto leading_surrogate = PrimitiveString::create(*test_vm.vm, *string, 0, 1);
|
|
auto trailing_surrogate = PrimitiveString::create(*test_vm.vm, *string, 1, 1);
|
|
|
|
(void)leading_surrogate->utf16_string_view();
|
|
(void)trailing_surrogate->utf16_string_view();
|
|
|
|
auto concatenated = PrimitiveString::create(*test_vm.vm, *leading_surrogate, *trailing_surrogate);
|
|
EXPECT(concatenated->utf16_string_view() == "😀"sv);
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_utf16_views_stay_deferred)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto string = PrimitiveString::create(*test_vm.vm, Utf16View { u"abcd", 4 });
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *string, 1, 2);
|
|
|
|
EXPECT(string->has_utf16_string());
|
|
EXPECT(!substring->has_utf16_string());
|
|
|
|
auto view = substring->utf16_string_view();
|
|
|
|
EXPECT_EQ(view.length_in_code_units(), 2u);
|
|
EXPECT_EQ(view.code_unit_at(0), 'b');
|
|
EXPECT_EQ(view.code_unit_at(1), 'c');
|
|
EXPECT(!substring->has_utf16_string());
|
|
}
|
|
|
|
TEST_CASE(primitive_string_substring_equality_uses_utf16_code_units)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
char16_t const source_code_units[] = { u'x', u'x', u'x', 0xd800, 0xdc00, 0xdc00, u'x', u'x' };
|
|
char16_t const substring_code_units[] = { 0xd800, 0xdc00, 0xdc00 };
|
|
|
|
auto source = PrimitiveString::create(*test_vm.vm, Utf16View { source_code_units, 8 });
|
|
auto substring = PrimitiveString::create(*test_vm.vm, *source, 3, 3);
|
|
auto expected = PrimitiveString::create(*test_vm.vm, Utf16View { substring_code_units, 3 });
|
|
|
|
EXPECT(*substring == *expected);
|
|
}
|
|
|
|
TEST_CASE(deferred_primitive_strings_do_not_evict_cached_strings)
|
|
{
|
|
TestVM test_vm;
|
|
|
|
auto cached_foo = PrimitiveString::create(*test_vm.vm, "foo"_utf16);
|
|
|
|
materialize_temporary_rope(*test_vm.vm);
|
|
clobber_stack();
|
|
|
|
test_vm.vm->heap().collect_garbage();
|
|
EXPECT_EQ(PrimitiveString::create(*test_vm.vm, "foo"_utf16).ptr(), cached_foo.ptr());
|
|
|
|
materialize_temporary_substring(*test_vm.vm);
|
|
clobber_stack();
|
|
|
|
test_vm.vm->heap().collect_garbage();
|
|
EXPECT_EQ(PrimitiveString::create(*test_vm.vm, "foo"_utf16).ptr(), cached_foo.ptr());
|
|
}
|