// Copyright 2021 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "src/codegen/code-stub-assembler.h" #include "src/codegen/cpu-features.h" #include "src/objects/objects-inl.h" #include "src/objects/swiss-name-dictionary-inl.h" #include "test/cctest/compiler/code-assembler-tester.h" #include "test/cctest/compiler/function-tester.h" #include "test/cctest/test-swiss-name-dictionary-infra.h" #include "test/cctest/test-swiss-name-dictionary-shared-tests.h" namespace v8 { namespace internal { namespace test_swiss_hash_table { // The non-SIMD SwissNameDictionary implementation requires 64 bit integer // operations, which CSA/Torque don't offer on 32 bit platforms. Therefore, we // cannot run the CSA version of the tests on 32 bit platforms. The only // exception is IA32, where we can use SSE and don't need 64 bit integers. // TODO(v8:11330) The Torque SIMD implementation is not specific to SSE (like // the C++ one), but works on other platforms. It should be possible to create a // workaround where on 32 bit, non-IA32 platforms we use the "portable", non-SSE // implementation on the C++ side (which uses a group size of 8) and create a // special version of the SIMD Torque implementation that works for group size 8 // instead of 16. #if V8_TARGET_ARCH_64_BIT || V8_TARGET_ARCH_IA32 // Executes tests by executing CSA/Torque versions of dictionary operations. // See RuntimeTestRunner for description of public functions. class CSATestRunner { public: CSATestRunner(Isolate* isolate, int initial_capacity, KeyCache& keys); // TODO(v8:11330): Remove once CSA implementation has a fallback for // non-SSSE3/AVX configurations. static bool IsEnabled() { #if V8_TARGET_ARCH_X64 || V8_TARGET_ARCH_IA32 CpuFeatures::SupportedFeatures(); return CpuFeatures::IsSupported(CpuFeature::AVX) || CpuFeatures::IsSupported(CpuFeature::SSSE3); #else // Other 64-bit architectures always support the required operations. return true; #endif } void Add(Handle key, Handle value, PropertyDetails details); InternalIndex FindEntry(Handle key); void Put(InternalIndex entry, Handle new_value, PropertyDetails new_details); void Delete(InternalIndex entry); void RehashInplace(); void Shrink(); Handle GetData(InternalIndex entry); void CheckCounts(base::Optional capacity, base::Optional elements, base::Optional deleted); void CheckEnumerationOrder(const std::vector& expected_keys); void CheckCopy(); void VerifyHeap(); void PrintTable(); Handle table; private: using Label = compiler::CodeAssemblerLabel; template using TVariable = compiler::TypedCodeAssemblerVariable; void CheckAgainstReference(); void Allocate(Handle capacity); Isolate* isolate_; // Used to mirror all operations using C++ versions of all operations, // yielding a reference to compare against. Handle reference_; // CSA functions execute the corresponding dictionary operation. compiler::FunctionTester find_entry_ft_; compiler::FunctionTester get_data_ft_; compiler::FunctionTester put_ft_; compiler::FunctionTester delete_ft_; compiler::FunctionTester add_ft_; compiler::FunctionTester allocate_ft_; compiler::FunctionTester get_counts_ft_; compiler::FunctionTester copy_ft_; // Used to create the FunctionTesters above. static Handle create_get_data(Isolate* isolate); static Handle create_find_entry(Isolate* isolate); static Handle create_put(Isolate* isolate); static Handle create_delete(Isolate* isolate); static Handle create_add(Isolate* isolate); static Handle create_allocate(Isolate* isolate); static Handle create_get_counts(Isolate* isolate); static Handle create_copy(Isolate* isolate); // Number of parameters of each of the tester functions above. static constexpr int kFindEntryParams = 2; // (table, key) static constexpr int kGetDataParams = 2; // (table, entry) static constexpr int kPutParams = 4; // (table, entry, value, details) static constexpr int kDeleteParams = 2; // (table, entry) static constexpr int kAddParams = 4; // (table, key, value, details) static constexpr int kAllocateParams = 1; // (capacity) static constexpr int kGetCountsParams = 1; // (table) static constexpr int kCopyParams = 1; // (table) }; CSATestRunner::CSATestRunner(Isolate* isolate, int initial_capacity, KeyCache& keys) : isolate_{isolate}, reference_{isolate_->factory()->NewSwissNameDictionaryWithCapacity( initial_capacity, AllocationType::kYoung)}, find_entry_ft_(create_find_entry(isolate), kFindEntryParams), get_data_ft_(create_get_data(isolate), kGetDataParams), put_ft_{create_put(isolate), kPutParams}, delete_ft_{create_delete(isolate), kDeleteParams}, add_ft_{create_add(isolate), kAddParams}, allocate_ft_{create_allocate(isolate), kAllocateParams}, get_counts_ft_{create_get_counts(isolate), kGetCountsParams}, copy_ft_{create_copy(isolate), kCopyParams} { Allocate(handle(Smi::FromInt(initial_capacity), isolate)); } void CSATestRunner::Add(Handle key, Handle value, PropertyDetails details) { ReadOnlyRoots roots(isolate_); reference_ = SwissNameDictionary::Add(isolate_, reference_, key, value, details); Handle details_smi = handle(details.AsSmi(), isolate_); Handle success = add_ft_.CallChecked(table, key, value, details_smi); if (*success == roots.false_value()) { // |add_ft_| does not resize and indicates the need to do so by returning // false. int capacity = table->Capacity(); int used_capacity = table->UsedCapacity(); CHECK_GT(used_capacity + 1, SwissNameDictionary::MaxUsableCapacity(capacity)); table = SwissNameDictionary::Add(isolate_, table, key, value, details); } CheckAgainstReference(); } void CSATestRunner::Allocate(Handle capacity) { // We must handle |capacity| == 0 specially, because // AllocateSwissNameDictionary (just like AllocateNameDictionary) always // returns a non-zero sized table. if (capacity->value() == 0) { table = ReadOnlyRoots(isolate_).empty_swiss_property_dictionary_handle(); } else { table = allocate_ft_.CallChecked(capacity); } CheckAgainstReference(); } InternalIndex CSATestRunner::FindEntry(Handle key) { Handle index = find_entry_ft_.CallChecked(table, key); if (index->value() == SwissNameDictionary::kNotFoundSentinel) { return InternalIndex::NotFound(); } else { return InternalIndex(index->value()); } } Handle CSATestRunner::GetData(InternalIndex entry) { DCHECK(entry.is_found()); return get_data_ft_.CallChecked( table, handle(Smi::FromInt(entry.as_int()), isolate_)); } void CSATestRunner::CheckCounts(base::Optional capacity, base::Optional elements, base::Optional deleted) { Handle counts = get_counts_ft_.CallChecked(table); if (capacity.has_value()) { CHECK_EQ(Smi::FromInt(capacity.value()), counts->get(0)); } if (elements.has_value()) { CHECK_EQ(Smi::FromInt(elements.value()), counts->get(1)); } if (deleted.has_value()) { CHECK_EQ(Smi::FromInt(deleted.value()), counts->get(2)); } CheckAgainstReference(); } void CSATestRunner::CheckEnumerationOrder( const std::vector& expected_keys) { // Not implemented in CSA. Making this a no-op (rather than forbidding // executing CSA tests with this operation) because CheckEnumerationOrder is // also used by some tests whose main goal is not to test the enumeration // order. } void CSATestRunner::Put(InternalIndex entry, Handle new_value, PropertyDetails new_details) { DCHECK(entry.is_found()); reference_->ValueAtPut(entry, *new_value); reference_->DetailsAtPut(entry, new_details); Handle entry_smi = handle(Smi::FromInt(entry.as_int()), isolate_); Handle details_smi = handle(new_details.AsSmi(), isolate_); put_ft_.Call(table, entry_smi, new_value, details_smi); CheckAgainstReference(); } void CSATestRunner::Delete(InternalIndex entry) { DCHECK(entry.is_found()); reference_ = SwissNameDictionary::DeleteEntry(isolate_, reference_, entry); Handle entry_smi = handle(Smi::FromInt(entry.as_int()), isolate_); table = delete_ft_.CallChecked(table, entry_smi); CheckAgainstReference(); } void CSATestRunner::RehashInplace() { // There's no CSA version of this. Use IsRuntimeTest to ensure that we only // run a test using this if it's a runtime test. UNREACHABLE(); } void CSATestRunner::Shrink() { // There's no CSA version of this. Use IsRuntimeTest to ensure that we only // run a test using this if it's a runtime test. UNREACHABLE(); } void CSATestRunner::CheckCopy() { Handle copy = copy_ft_.CallChecked(table); CHECK(table->EqualsForTesting(*copy)); } void CSATestRunner::VerifyHeap() { #if VERIFY_HEAP table->SwissNameDictionaryVerify(isolate_, true); #endif } void CSATestRunner::PrintTable() { #ifdef OBJECT_PRINT table->SwissNameDictionaryPrint(std::cout); #endif } Handle CSATestRunner::create_find_entry(Isolate* isolate) { // TODO(v8:11330): Remove once CSA implementation has a fallback for // non-SSSE3/AVX configurations. if (!IsEnabled()) { return FromCodeT(isolate->builtins()->code_handle(Builtin::kIllegal), isolate); } static_assert(kFindEntryParams == 2); // (table, key) compiler::CodeAssemblerTester asm_tester(isolate, kFindEntryParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode key = m.Parameter(2); Label done(&m); TVariable entry_var( m.IntPtrConstant(SwissNameDictionary::kNotFoundSentinel), &m); // |entry_var| defaults to |kNotFoundSentinel| meaning that one label // suffices. m.SwissNameDictionaryFindEntry(table, key, &done, &entry_var, &done); m.Bind(&done); m.Return(m.SmiFromIntPtr(entry_var.value())); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_get_data(Isolate* isolate) { static_assert(kGetDataParams == 2); // (table, entry) compiler::CodeAssemblerTester asm_tester(isolate, kGetDataParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode entry = m.SmiToIntPtr(m.Parameter(2)); TNode data = m.AllocateZeroedFixedArray(m.IntPtrConstant(3)); TNode key = m.LoadSwissNameDictionaryKey(table, entry); TNode value = m.LoadValueByKeyIndex(table, entry); TNode details = m.SmiFromUint32(m.LoadDetailsByKeyIndex(table, entry)); m.StoreFixedArrayElement(data, 0, key); m.StoreFixedArrayElement(data, 1, value); m.StoreFixedArrayElement(data, 2, details); m.Return(data); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_put(Isolate* isolate) { static_assert(kPutParams == 4); // (table, entry, value, details) compiler::CodeAssemblerTester asm_tester(isolate, kPutParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode entry = m.Parameter(2); TNode value = m.Parameter(3); TNode details = m.Parameter(4); TNode entry_intptr = m.SmiToIntPtr(entry); m.StoreValueByKeyIndex(table, entry_intptr, value, WriteBarrierMode::UPDATE_WRITE_BARRIER); m.StoreDetailsByKeyIndex(table, entry_intptr, details); m.Return(m.UndefinedConstant()); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_delete(Isolate* isolate) { // TODO(v8:11330): Remove once CSA implementation has a fallback for // non-SSSE3/AVX configurations. if (!IsEnabled()) { return FromCodeT(isolate->builtins()->code_handle(Builtin::kIllegal), isolate); } static_assert(kDeleteParams == 2); // (table, entry) compiler::CodeAssemblerTester asm_tester(isolate, kDeleteParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode entry = m.SmiToIntPtr(m.Parameter(2)); TVariable shrunk_table_var(table, &m); Label done(&m); m.SwissNameDictionaryDelete(table, entry, &done, &shrunk_table_var); m.Goto(&done); m.Bind(&done); m.Return(shrunk_table_var.value()); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_add(Isolate* isolate) { // TODO(v8:11330): Remove once CSA implementation has a fallback for // non-SSSE3/AVX configurations. if (!IsEnabled()) { return FromCodeT(isolate->builtins()->code_handle(Builtin::kIllegal), isolate); } static_assert(kAddParams == 4); // (table, key, value, details) compiler::CodeAssemblerTester asm_tester(isolate, kAddParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode key = m.Parameter(2); TNode value = m.Parameter(3); TNode details = m.Parameter(4); Label needs_resize(&m); TNode d32 = m.SmiToInt32(details); TNode d = m.UncheckedCast(d32); m.SwissNameDictionaryAdd(table, key, value, d, &needs_resize); m.Return(m.TrueConstant()); m.Bind(&needs_resize); m.Return(m.FalseConstant()); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_allocate(Isolate* isolate) { static_assert(kAllocateParams == 1); // (capacity) compiler::CodeAssemblerTester asm_tester(isolate, kAllocateParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode capacity = m.SmiToIntPtr(m.Parameter(1)); TNode table = m.AllocateSwissNameDictionaryWithCapacity(capacity); m.Return(table); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_get_counts(Isolate* isolate) { static_assert(kGetCountsParams == 1); // (table) compiler::CodeAssemblerTester asm_tester(isolate, kGetCountsParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); TNode capacity = m.ChangeInt32ToIntPtr(m.LoadSwissNameDictionaryCapacity(table)); TNode elements = m.LoadSwissNameDictionaryNumberOfElements(table, capacity); TNode deleted = m.LoadSwissNameDictionaryNumberOfDeletedElements(table, capacity); TNode results = m.AllocateZeroedFixedArray(m.IntPtrConstant(3)); auto check_and_add = [&](TNode value, int array_index) { CSA_DCHECK(&m, m.UintPtrGreaterThanOrEqual(value, m.IntPtrConstant(0))); CSA_DCHECK(&m, m.UintPtrLessThanOrEqual( value, m.IntPtrConstant(Smi::kMaxValue))); TNode smi = m.SmiFromIntPtr(value); m.StoreFixedArrayElement(results, array_index, smi); }; check_and_add(capacity, 0); check_and_add(elements, 1); check_and_add(deleted, 2); m.Return(results); } return asm_tester.GenerateCodeCloseAndEscape(); } Handle CSATestRunner::create_copy(Isolate* isolate) { static_assert(kCopyParams == 1); // (table) compiler::CodeAssemblerTester asm_tester(isolate, kCopyParams + 1); CodeStubAssembler m(asm_tester.state()); { TNode table = m.Parameter(1); m.Return(m.CopySwissNameDictionary(table)); } return asm_tester.GenerateCodeCloseAndEscape(); } void CSATestRunner::CheckAgainstReference() { CHECK(table->EqualsForTesting(*reference_)); } // Executes the tests defined in test-swiss-name-dictionary-shared-tests.h as if // they were defined in this file, using the CSATestRunner. See comments in // test-swiss-name-dictionary-shared-tests.h and in // swiss-name-dictionary-infra.h for details. const char kCSATestFileName[] = __FILE__; SharedSwissTableTests execute_shared_tests_csa; #endif } // namespace test_swiss_hash_table } // namespace internal } // namespace v8