Commit 1dff0822 authored by Frank Emrich's avatar Frank Emrich Committed by Commit Bot

[dict-proto] C++ implementation of SwissNameDictionary, pt. 9

This CL is part of a series that adds the C++ implementation of
SwissNameDictionary, a deterministic property backing store based on
Swiss Tables.

This CL adds test-swiss-name-dictionary-infra.[h|cc], which contain
the infrastructure for writing tests that simulatenously check the
C++ and CSA/Torque implementation of SwissNameDictionary operations.

The actual tests are added in a subsequent CL, which will be the last of
this series.

Bug: v8:11388
Change-Id: I89cbc7e575ed694fe34cb66c0e1ec70683504bd8
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2742574Reviewed-by: 's avatarIgor Sheludko <ishell@chromium.org>
Reviewed-by: 's avatarMarja Hölttä <marja@chromium.org>
Commit-Queue: Frank Emrich <emrich@google.com>
Cr-Commit-Position: refs/heads/master@{#73516}
parent 89b3cd33
......@@ -1423,10 +1423,10 @@ RUNTIME_FUNCTION(Runtime_SwissTableUpdate) {
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableDelete) {
HandleScope scope(isolate);
Handle<SwissNameDictionary> table = args.at<SwissNameDictionary>(0);
Handle<Smi> index = args.at<Smi>(1);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
CONVERT_ARG_HANDLE_CHECKED(Smi, entry, 1);
InternalIndex i(Smi::ToInt(*index));
InternalIndex i(Smi::ToInt(*entry));
return *SwissNameDictionary::DeleteEntry(isolate, table, i);
}
......@@ -1435,8 +1435,8 @@ RUNTIME_FUNCTION(Runtime_SwissTableDelete) {
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableEquals) {
HandleScope scope(isolate);
Handle<SwissNameDictionary> table = args.at<SwissNameDictionary>(0);
Handle<SwissNameDictionary> other = args.at<SwissNameDictionary>(1);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, other, 1);
return Smi::FromInt(table->EqualsForTesting(*other));
}
......@@ -1445,29 +1445,39 @@ RUNTIME_FUNCTION(Runtime_SwissTableEquals) {
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableElementsCount) {
HandleScope scope(isolate);
Handle<SwissNameDictionary> table = args.at<SwissNameDictionary>(0);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
return Smi::FromInt(table->NumberOfElements());
}
// TODO(v8:11330) This is only here while the CSA/Torque implementaton of
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableKeyAt) {
HandleScope scope(isolate);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
CONVERT_ARG_HANDLE_CHECKED(Smi, entry, 1);
return table->KeyAt(InternalIndex(Smi::ToInt(*entry)));
}
// TODO(v8:11330) This is only here while the CSA/Torque implementaton of
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableValueAt) {
HandleScope scope(isolate);
Handle<SwissNameDictionary> table = args.at<SwissNameDictionary>(0);
Handle<Smi> index = args.at<Smi>(1);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
CONVERT_ARG_HANDLE_CHECKED(Smi, entry, 1);
return table->ValueAt(InternalIndex(Smi::ToInt(*index)));
return table->ValueAt(InternalIndex(Smi::ToInt(*entry)));
}
// TODO(v8:11330) This is only here while the CSA/Torque implementaton of
// SwissNameDictionary is work in progress.
RUNTIME_FUNCTION(Runtime_SwissTableDetailsAt) {
HandleScope scope(isolate);
Handle<SwissNameDictionary> table = args.at<SwissNameDictionary>(0);
Handle<Smi> index = args.at<Smi>(1);
CONVERT_ARG_HANDLE_CHECKED(SwissNameDictionary, table, 0);
CONVERT_ARG_HANDLE_CHECKED(Smi, entry, 1);
PropertyDetails d = table->DetailsAt(InternalIndex(Smi::ToInt(*index)));
PropertyDetails d = table->DetailsAt(InternalIndex(Smi::ToInt(*entry)));
return d.AsSmi();
}
......
......@@ -352,7 +352,8 @@ namespace internal {
F(SwissTableEquals, 2, 1) \
F(SwissTableFindEntry, 2, 1) \
F(SwissTableUpdate, 4, 1) \
F(SwissTableValueAt, 2, 1)
F(SwissTableValueAt, 2, 1) \
F(SwissTableKeyAt, 2, 1)
#define FOR_EACH_INTRINSIC_OPERATORS(F, I) \
F(Add, 2, 1) \
......
......@@ -269,6 +269,8 @@ v8_source_set("cctest_sources") {
"test-smi-lexicographic-compare.cc",
"test-strings.cc",
"test-strtod.cc",
"test-swiss-name-dictionary-csa.cc",
"test-swiss-name-dictionary-infra.cc",
"test-swiss-name-dictionary.cc",
"test-symbols.cc",
"test-thread-termination.cc",
......
......@@ -599,6 +599,7 @@
'test-run-variables/*': [SKIP],
'test-serialize/*': [SKIP],
'test-sloppy-equality/*' : [SKIP],
'test-swiss-name-dictionary-csa/*': [SKIP],
'test-torque/*': [SKIP],
'test-unwinder-code-pages/PCIsInV8_LargeCodeObject_CodePagesAPI': [SKIP],
......
This diff is collapsed.
// 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 "test/cctest/test-swiss-name-dictionary-infra.h"
namespace v8 {
namespace internal {
namespace test_swiss_hash_table {
namespace {
std::vector<PropertyDetails> MakeDistinctDetails() {
std::vector<PropertyDetails> result(32, PropertyDetails::Empty());
int i = 0;
for (PropertyKind kind : {PropertyKind::kAccessor, PropertyKind::kAccessor}) {
for (PropertyConstness constness :
{PropertyConstness::kConst, PropertyConstness::kMutable}) {
for (bool writeable : {true, false}) {
for (bool enumerable : {true, false}) {
for (bool configurable : {true, false}) {
uint8_t attrs = static_cast<uint8_t>(PropertyAttributes::NONE);
if (!writeable) attrs |= PropertyAttributes::READ_ONLY;
if (!enumerable) {
attrs |= PropertyAttributes::DONT_ENUM;
}
if (!configurable) {
attrs |= PropertyAttributes::DONT_DELETE;
}
PropertyAttributes attributes =
static_cast<PropertyAttributes>(attrs);
PropertyDetails details(kind, attributes,
PropertyCellType::kNoCell);
details = details.CopyWithConstness(constness);
result[i++] = details;
}
}
}
}
}
return result;
}
} // namespace
// To enable more specific testing, we allow overriding the H1 and H2 hashes for
// a key before adding it to the SwissNameDictionary. The necessary overriding
// of the stored hash happens here. Symbols are compared by identity, we cache
// the Symbol associcated with each std::string key. This means that using
// "my_key" twice in the same TestSequence will return the same Symbol
// associcated with "my_key" both times. This also means that within a given
// TestSequence, we cannot use the same (std::string) key with different faked
// hashes.
Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
const Key& key) {
Handle<Symbol> key_symbol;
auto iter = keys.find(key.str);
if (iter == keys.end()) {
// We haven't seen the the given string as a key in the current
// TestSequence. Create it, fake its hash if requested and cache it.
key_symbol = isolate->factory()->NewSymbol();
// We use the description field to store the original string key for
// debugging.
Handle<String> description =
isolate->factory()->NewStringFromAsciiChecked(key.str.c_str());
key_symbol->set_description(*description);
CachedKey new_info = {key_symbol, key.h1_override, key.h2_override};
keys[key.str] = new_info;
if (key.h1_override || key.h2_override) {
uint32_t actual_hash = key_symbol->hash();
int fake_hash = actual_hash;
if (key.h1_override) {
uint32_t override_with = key.h1_override.value().value;
fake_hash = (override_with << swiss_table::kH2Bits) |
swiss_table::H2(actual_hash);
}
if (key.h2_override) {
// Unset 7 bits belonging to H2:
fake_hash &= ~((1 << swiss_table::kH2Bits) - 1);
DCHECK_LT(key.h2_override.value().value, 1 << swiss_table::kH2Bits);
fake_hash |= swiss_table::H2(key.h2_override.value().value);
}
// Ensure that just doing a shift below is correct.
static_assert(Name::kNofHashBitFields == 2, "This test needs updating");
static_assert(Name::kHashNotComputedMask == 1,
"This test needs updating");
static_assert(Name::kIsNotIntegerIndexMask == 2,
"This test needs updating");
// Prepare what to put into the hash field.
uint32_t hash_field = fake_hash << Name::kHashShift;
key_symbol->set_raw_hash_field(hash_field);
DCHECK_EQ(fake_hash, key_symbol->hash());
}
return key_symbol;
} else {
// We've seen this key before. Return the cached version.
CachedKey& cached_info = iter->second;
// Internal consistency check: Make sure that we didn't request something
// else w.r.t. hash faking when using this key before. If so, the test case
// would make inconsistent assumptions about how the hashes should be faked
// and be broken.
DCHECK_EQ(cached_info.h1_override, key.h1_override);
DCHECK_EQ(cached_info.h2_override, key.h2_override);
return cached_info.key_symbol;
}
}
const std::vector<PropertyDetails> distinct_property_details =
MakeDistinctDetails();
} // namespace test_swiss_hash_table
} // namespace internal
} // namespace v8
// 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.
#ifndef V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_
#define V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_
#include <memory>
#include <utility>
#include "src/codegen/code-stub-assembler.h"
#include "src/init/v8.h"
#include "src/objects/objects-inl.h"
#include "src/objects/swiss-name-dictionary-inl.h"
#include "test/cctest/cctest.h"
#include "test/cctest/compiler/code-assembler-tester.h"
#include "test/cctest/compiler/function-tester.h"
namespace v8 {
namespace internal {
namespace test_swiss_hash_table {
using Value = std::string;
using ValueOpt = base::Optional<Value>;
using PropertyDetailsOpt = base::Optional<PropertyDetails>;
using IndexOpt = base::Optional<InternalIndex>;
static const ValueOpt kNoValue;
static const PropertyDetailsOpt kNoDetails;
static const base::Optional<int> kNoInt;
static const IndexOpt kNoIndex;
static const std::vector<int> interesting_initial_capacities = {
4,
8,
16,
128,
1 << (sizeof(uint16_t) * 8),
1 << (sizeof(uint16_t) * 8 + 1)};
extern const std::vector<PropertyDetails> distinct_property_details;
// Wrapping this in a struct makes the tests a bit more readable.
struct FakeH1 {
uint32_t value;
explicit FakeH1(int value) : value{static_cast<uint32_t>(value)} {}
bool operator==(const FakeH1& other) const { return value == other.value; }
};
// Wrapping this in a struct makes the tests a bit more readable.
struct FakeH2 {
uint8_t value;
bool operator==(const FakeH2& other) const { return value == other.value; }
};
using FakeH1Opt = base::Optional<FakeH1>;
using FakeH2Opt = base::Optional<FakeH2>;
// Representation of keys used when writing test cases.
struct Key {
std::string str;
// If present, contains the value we faked the key's H1 hash with.
FakeH1Opt h1_override = FakeH1Opt();
// If present, contains the value we faked the key's H2 hash with.
FakeH2Opt h2_override = FakeH2Opt();
};
// Internal representation of keys. See |create_key_with_hash| for details.
struct CachedKey {
Handle<Symbol> key_symbol;
// If present, contains the value we faked the key's H1 hash with.
FakeH1Opt h1_override;
// If present, contains the value we faked the key's H2 hash with.
FakeH2Opt h2_override;
};
using KeyCache = std::unordered_map<std::string, CachedKey>;
Handle<Name> CreateKeyWithHash(Isolate* isolate, KeyCache& keys,
const Key& key);
class RuntimeTestRunner;
class CSATestRunner;
// Abstraction over executing a sequence of operations on a single hash table.
// Actually performing those operations is done by the TestRunner.
template <typename TestRunner>
class TestSequence {
public:
explicit TestSequence(Isolate* isolate, int initial_capacity)
: isolate{isolate},
initial_capacity{initial_capacity},
keys_{},
runner_{isolate, initial_capacity, keys_} {}
// Determines whether or not to run VerifyHeap after each operation. Can make
// debugging easier.
static constexpr bool kVerifyAfterEachStep = false;
void Add(Handle<Name> key, Handle<Object> value, PropertyDetails details) {
runner_.Add(key, value, details);
if (kVerifyAfterEachStep) {
runner_.VerifyHeap();
}
}
void Add(const Key& key, ValueOpt value = kNoValue,
PropertyDetailsOpt details = kNoDetails) {
if (!value) {
value = "dummy_value";
}
if (!details) {
details = PropertyDetails::Empty();
}
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, key);
Handle<Object> value_handle = isolate->factory()->NewStringFromAsciiChecked(
value.value().c_str(), AllocationType::kYoung);
Add(key_handle, value_handle, details.value());
}
void UpdateByKey(Handle<Name> key, Handle<Object> new_value,
PropertyDetails new_details) {
InternalIndex entry = runner_.FindEntry(key);
CHECK(entry.is_found());
runner_.Put(entry, new_value, new_details);
if (kVerifyAfterEachStep) {
runner_.VerifyHeap();
}
}
void UpdateByKey(const Key& existing_key, Value new_value,
PropertyDetails new_details) {
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, existing_key);
Handle<Object> value_handle = isolate->factory()->NewStringFromAsciiChecked(
new_value.c_str(), AllocationType::kYoung);
UpdateByKey(key_handle, value_handle, new_details);
}
void DeleteByKey(Handle<Name> key) {
InternalIndex entry = runner_.FindEntry(key);
CHECK(entry.is_found());
runner_.Delete(entry);
if (kVerifyAfterEachStep) {
runner_.VerifyHeap();
}
}
void DeleteByKey(const Key& existing_key) {
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, existing_key);
DeleteByKey(key_handle);
}
void CheckDataAtKey(Handle<Name> key, IndexOpt expected_index_opt,
base::Optional<Handle<Object>> expected_value_opt,
PropertyDetailsOpt expected_details_opt) {
InternalIndex actual_index = runner_.FindEntry(key);
if (expected_index_opt) {
CHECK_EQ(expected_index_opt.value(), actual_index);
}
if (actual_index.is_found()) {
Handle<FixedArray> data = runner_.GetData(actual_index);
CHECK_EQ(*key, data->get(0));
if (expected_value_opt) {
CHECK(expected_value_opt.value()->StrictEquals(data->get(1)));
}
if (expected_details_opt) {
CHECK_EQ(expected_details_opt.value().AsSmi(), data->get(2));
}
}
}
void CheckDataAtKey(const Key& expected_key, IndexOpt expected_index,
ValueOpt expected_value = kNoValue,
PropertyDetailsOpt expected_details = kNoDetails) {
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);
base::Optional<Handle<Object>> value_handle_opt;
if (expected_value) {
value_handle_opt = isolate->factory()->NewStringFromAsciiChecked(
expected_value.value().c_str(), AllocationType::kYoung);
}
CheckDataAtKey(key_handle, expected_index, value_handle_opt,
expected_details);
}
void CheckKeyAbsent(Handle<Name> key) {
CHECK(runner_.FindEntry(key).is_not_found());
}
void CheckKeyAbsent(const Key& expected_key) {
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);
CheckKeyAbsent(key_handle);
}
void CheckHasKey(const Key& expected_key) {
Handle<Name> key_handle = CreateKeyWithHash(isolate, keys_, expected_key);
CHECK(runner_.FindEntry(key_handle).is_found());
}
void CheckCounts(base::Optional<int> capacity,
base::Optional<int> elements = base::Optional<int>(),
base::Optional<int> deleted = base::Optional<int>()) {
runner_.CheckCounts(capacity, elements, deleted);
}
void CheckEnumerationOrder(const std::vector<std::string>& keys) {
runner_.CheckEnumerationOrder(keys);
}
void RehashInplace() { runner_.RehashInplace(); }
void Shrink() { runner_.Shrink(); }
void CheckCopy() { runner_.CheckCopy(); }
static constexpr bool IsRuntimeTest() {
return std::is_same<TestRunner, RuntimeTestRunner>::value;
}
void VerifyHeap() { runner_.VerifyHeap(); }
// Just for debugging
void Print() { runner_.PrintTable(); }
static std::vector<int> boundary_indices(int capacity) {
if (capacity == 4 && SwissNameDictionary::MaxUsableCapacity(4) < 4) {
// If we cannot put 4 entries in a capacity 4 table without resizing, just
// work with 3 boundary indices.
return {0, capacity - 2, capacity - 1};
}
return {0, 1, capacity - 2, capacity - 1};
}
// Contains all possible PropertyDetails suitable for storing in a
// SwissNameDictionary (i.e., PropertyDetails for dictionary mode objects
// without storing an enumeration index). Used to ensure that we can correctly
// store an retrieve all possible such PropertyDetails.
static const std::vector<PropertyDetails> distinct_property_details;
static void WithAllInterestingInitialCapacities(
std::function<void(TestSequence&)> manipulate_sequence) {
WithInitialCapacities(interesting_initial_capacities, manipulate_sequence);
}
static void WithInitialCapacity(
int capacity, std::function<void(TestSequence&)> manipulate_sequence) {
WithInitialCapacities({capacity}, manipulate_sequence);
}
// For each capacity in |capacities|, create a TestSequence and run the given
// function on it.
static void WithInitialCapacities(
const std::vector<int>& capacities,
std::function<void(TestSequence&)> manipulate_sequence) {
for (int capacity : capacities) {
Isolate* isolate = CcTest::InitIsolateOnce();
HandleScope scope{isolate};
TestSequence<TestRunner> s(isolate, capacity);
manipulate_sequence(s);
}
}
Isolate* const isolate;
const int initial_capacity;
private:
// Caches keys used in this TestSequence. See |create_key_with_hash| for
// details.
KeyCache keys_;
TestRunner runner_;
};
} // namespace test_swiss_hash_table
} // namespace internal
} // namespace v8
#endif // V8_TEST_CCTEST_TEST_SWISS_NAME_DICTIONARY_INFRA_H_
......@@ -4,11 +4,148 @@
#include "src/objects/swiss-name-dictionary-inl.h"
#include "test/cctest/cctest.h"
#include "test/cctest/test-swiss-name-dictionary-infra.h"
namespace v8 {
namespace internal {
namespace test_swiss_hash_table {
// Executes tests by executing C++ versions of dictionary operations.
class RuntimeTestRunner {
public:
RuntimeTestRunner(Isolate* isolate, int initial_capacity, KeyCache& keys)
: isolate_{isolate}, keys_{keys} {
table = isolate->factory()->NewSwissNameDictionaryWithCapacity(
initial_capacity, AllocationType::kYoung);
}
void Add(Handle<Name> key, Handle<Object> value, PropertyDetails details);
InternalIndex FindEntry(Handle<Name> key);
// Updates the value and property details of the given entry.
void Put(InternalIndex entry, Handle<Object> new_value,
PropertyDetails new_details);
void Delete(InternalIndex entry);
void RehashInplace();
void Shrink();
// Retrieves data associated with |entry|, which must be an index pointing to
// an existing entry. The returned array contains key, value, property details
// in that order.
Handle<FixedArray> GetData(InternalIndex entry);
// Tests that the current table has the given capacity, and number of
// (deleted) elements, based on which optional values are present.
void CheckCounts(base::Optional<int> capacity, base::Optional<int> elements,
base::Optional<int> deleted);
// Checks that |expected_keys| contains exactly the keys in the current table,
// in the given order.
void CheckEnumerationOrder(const std::vector<std::string>& expected_keys);
void CheckCopy();
void VerifyHeap();
// Just for debugging.
void PrintTable();
Handle<SwissNameDictionary> table;
private:
Isolate* isolate_;
KeyCache& keys_;
};
void RuntimeTestRunner::Add(Handle<Name> key, Handle<Object> value,
PropertyDetails details) {
Handle<SwissNameDictionary> updated_table =
SwissNameDictionary::Add(isolate_, this->table, key, value, details);
this->table = updated_table;
}
InternalIndex RuntimeTestRunner::FindEntry(Handle<Name> key) {
return table->FindEntry(isolate_, key);
}
Handle<FixedArray> RuntimeTestRunner::GetData(InternalIndex entry) {
if (entry.is_found()) {
Handle<FixedArray> data = isolate_->factory()->NewFixedArray(3);
data->set(0, table->KeyAt(entry));
data->set(1, table->ValueAt(entry));
data->set(2, table->DetailsAt(entry).AsSmi());
return data;
} else {
return handle(ReadOnlyRoots(isolate_).empty_fixed_array(), isolate_);
}
}
void RuntimeTestRunner::Put(InternalIndex entry, Handle<Object> new_value,
PropertyDetails new_details) {
CHECK(entry.is_found());
table->ValueAtPut(entry, *new_value);
table->DetailsAtPut(entry, new_details);
}
void RuntimeTestRunner::Delete(InternalIndex entry) {
CHECK(entry.is_found());
table = table->DeleteEntry(isolate_, table, entry);
}
void RuntimeTestRunner::CheckCounts(base::Optional<int> capacity,
base::Optional<int> elements,
base::Optional<int> deleted) {
if (capacity.has_value()) {
CHECK_EQ(capacity.value(), table->Capacity());
}
if (elements.has_value()) {
CHECK_EQ(elements.value(), table->NumberOfElements());
}
if (deleted.has_value()) {
CHECK_EQ(deleted.value(), table->NumberOfDeletedElements());
}
}
void RuntimeTestRunner::CheckEnumerationOrder(
const std::vector<std::string>& expected_keys) {
ReadOnlyRoots roots(isolate_);
int i = 0;
for (InternalIndex index : table->IterateEntriesOrdered()) {
Object key;
if (table->ToKey(roots, index, &key)) {
CHECK_LT(i, expected_keys.size());
Handle<Name> expected_key =
CreateKeyWithHash(isolate_, this->keys_, Key{expected_keys[i]});
CHECK_EQ(key, *expected_key);
++i;
}
}
CHECK_EQ(i, expected_keys.size());
}
void RuntimeTestRunner::RehashInplace() { table->Rehash(isolate_); }
void RuntimeTestRunner::Shrink() {
table = SwissNameDictionary::Shrink(isolate_, table);
}
void RuntimeTestRunner::CheckCopy() {
Handle<SwissNameDictionary> copy =
SwissNameDictionary::ShallowCopy(isolate_, table);
CHECK(table->EqualsForTesting(*copy));
}
void RuntimeTestRunner::VerifyHeap() {
#if VERIFY_HEAP
table->SwissNameDictionaryVerify(isolate_, true);
#endif
}
void RuntimeTestRunner::PrintTable() {
#ifdef OBJECT_PRINT
table->SwissNameDictionaryPrint(std::cout);
#endif
}
TEST(CapacityFor) {
for (int elements = 0; elements <= 32; elements++) {
int capacity = SwissNameDictionary::CapacityFor(elements);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment