unittests/VMRuntime/HiddenClassTest.cpp (365 lines of code) (raw):
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "hermes/VM/HiddenClass.h"
#include "hermes/VM/ArrayStorage.h"
#include "hermes/VM/Runtime.h"
#include "TestHelpers.h"
#include "gtest/gtest.h"
using namespace hermes::vm;
namespace {
using HiddenClassTest = LargeHeapRuntimeTestFixture;
TEST_F(HiddenClassTest, SmokeTest) {
GCScope gcScope{runtime, "HiddenClassTest.SmokeTest", 48};
runtime.collect("test");
auto aHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"a"));
auto bHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"b"));
auto cHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"c"));
auto dHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"d"));
// We will simulate and verify the following property operations, starting
// from the same root.
/// x.a, x.b
/// y.a, y.b
/// x.c
/// y.d
/// read-only x.b
/// z.a, z.b, z.c
/// read-only z.b
/// all-read-only y twice
MutableHandle<HiddenClass> x{runtime};
MutableHandle<HiddenClass> y{runtime};
MutableHandle<HiddenClass> z{runtime};
auto rootHnd = runtime.makeHandle<HiddenClass>(
runtime.ignoreAllocationFailure(HiddenClass::createRoot(runtime)));
ASSERT_EQ(0u, rootHnd->getNumProperties());
ASSERT_FALSE(rootHnd->isDictionary());
ASSERT_TRUE(rootHnd->isKnownLeaf());
// x = {}
x = *rootHnd;
{
// x.a
auto addRes = HiddenClass::addProperty(
x, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
ASSERT_NE(*rootHnd, *addRes->first);
x = *addRes->first;
}
{
// x.b
auto addRes = HiddenClass::addProperty(
x, runtime, *bHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(1u, addRes->second);
ASSERT_NE(*x, *addRes->first);
x = *addRes->first;
}
// y = {}
y = *rootHnd;
{
// y.a
auto addRes = HiddenClass::addProperty(
y, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
y = *addRes->first;
}
{
// y.b
auto addRes = HiddenClass::addProperty(
y, runtime, *bHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(1u, addRes->second);
y = *addRes->first;
ASSERT_EQ(*x, *y);
}
{
// x.c
auto addRes = HiddenClass::addProperty(
x, runtime, *cHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(2u, addRes->second);
ASSERT_NE(*x, *addRes->first);
x = *addRes->first;
ASSERT_EQ(3u, x->getNumProperties());
ASSERT_FALSE(x->isDictionary());
ASSERT_TRUE(x->isKnownLeaf());
}
{
// y.d
auto addRes = HiddenClass::addProperty(
y, runtime, *dHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(2u, addRes->second);
y = *addRes->first;
ASSERT_NE(*x, *y);
ASSERT_EQ(3u, y->getNumProperties());
ASSERT_FALSE(y->isDictionary());
ASSERT_TRUE(y->isKnownLeaf());
}
// Find all properties in x.
NamedPropertyDescriptor desc;
{
auto found = HiddenClass::findProperty(
x, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(0u, desc.slot);
found = HiddenClass::findProperty(
x, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
found = HiddenClass::findProperty(
x, runtime, *cHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(2u, desc.slot);
found = HiddenClass::findProperty(
x, runtime, *dHnd, PropertyFlags::invalid(), desc);
ASSERT_FALSE(found);
}
{
// Read-only x.b
auto found = HiddenClass::findProperty(
x, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_TRUE(desc.flags.writable);
desc.flags.writable = false;
auto newClz = HiddenClass::updateProperty(x, runtime, *found, desc.flags);
ASSERT_NE(*x, *newClz);
ASSERT_EQ(x->getNumProperties(), newClz->getNumProperties());
x = *newClz;
found = HiddenClass::findProperty(
x, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
}
// z = {}
z = *rootHnd;
{
// z.a
auto addRes = HiddenClass::addProperty(
z, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
z = *addRes->first;
}
{
// z.b
auto addRes = HiddenClass::addProperty(
z, runtime, *bHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(1u, addRes->second);
z = *addRes->first;
}
{
// z.c
auto addRes = HiddenClass::addProperty(
z, runtime, *cHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(2u, addRes->second);
z = *addRes->first;
}
{
// Read-only z.b
auto found = HiddenClass::findProperty(
z, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_TRUE(desc.flags.writable);
desc.flags.writable = false;
auto newClz = HiddenClass::updateProperty(z, runtime, *found, desc.flags);
ASSERT_NE(*z, *newClz);
ASSERT_EQ(z->getNumProperties(), newClz->getNumProperties());
z = *newClz;
found = HiddenClass::findProperty(
z, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_EQ(*x, *z);
}
auto y1 = HiddenClass::makeAllReadOnly(y, runtime);
auto y2 = HiddenClass::makeAllReadOnly(y, runtime);
ASSERT_EQ(*y1, *y2);
auto y3 = HiddenClass::makeAllReadOnly(y1, runtime);
ASSERT_EQ(*y1, *y3);
// Turn x into a dictionary by erasing x.a
{
auto found = HiddenClass::findProperty(
x, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(0u, desc.slot);
auto x1 = HiddenClass::deleteProperty(x, runtime, *found);
ASSERT_NE(*x, *x1);
ASSERT_FALSE(x->isDictionary());
ASSERT_TRUE(x1->isDictionary());
ASSERT_EQ(2u, x1->getNumProperties());
found = HiddenClass::findProperty(
x1, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_FALSE(found);
x = *x1;
}
{
// x.a (again)
auto addRes = HiddenClass::addProperty(
x, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
ASSERT_EQ(*x, *addRes->first);
ASSERT_EQ(3u, x->getNumProperties());
}
}
TEST_F(HiddenClassTest, UpdatePropertyFlagsWithoutTransitionsTest) {
GCScope gcScope{
runtime, "HiddenClassTest.UpdatePropertyFlagsWithoutTransitionsTest", 48};
runtime.collect("test");
auto aHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"a"));
auto bHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"b"));
auto cHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"c"));
auto dHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"d"));
// Add y.a, y.b, y.c
MutableHandle<HiddenClass> y{
runtime,
vmcast<HiddenClass>(
runtime.ignoreAllocationFailure(HiddenClass::createRoot(runtime)))};
{
// y.a
auto addRes = HiddenClass::addProperty(
y, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
y = *addRes->first;
}
{
// y.b
auto addRes = HiddenClass::addProperty(
y, runtime, *bHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(1u, addRes->second);
y = *addRes->first;
}
{
// y.c
auto addRes = HiddenClass::addProperty(
y, runtime, *cHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(2u, addRes->second);
y = *addRes->first;
}
NamedPropertyDescriptor desc;
PropertyFlags clearFlags;
clearFlags.writable = 1;
clearFlags.configurable = 1;
PropertyFlags setFlags;
ASSERT_FALSE(y->isDictionary());
// y is not a dictionary so we will get a new hidden class.
auto yClone = HiddenClass::updatePropertyFlagsWithoutTransitions(
y, runtime, clearFlags, setFlags, llvh::None);
ASSERT_NE(*y, *yClone);
ASSERT_EQ(y->getNumProperties(), yClone->getNumProperties());
ASSERT_TRUE(yClone->isDictionary());
// Check each property
auto found = HiddenClass::findProperty(
yClone, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(0u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_FALSE(desc.flags.configurable);
found = HiddenClass::findProperty(
yClone, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_FALSE(desc.flags.configurable);
found = HiddenClass::findProperty(
yClone, runtime, *cHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(2u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_FALSE(desc.flags.configurable);
// Turn y into a dictionary y3 by deleting y.a
found = HiddenClass::findProperty(
y, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(0u, desc.slot);
auto y3 = HiddenClass::deleteProperty(y, runtime, *found);
ASSERT_NE(*y, *y3);
ASSERT_TRUE(y3->isDictionary());
ASSERT_EQ(2u, y3->getNumProperties());
// We should not create a new hidden class in this case.
auto y4 = HiddenClass::updatePropertyFlagsWithoutTransitions(
y3, runtime, clearFlags, setFlags, llvh::None);
ASSERT_EQ(*y4, *y3);
// Only freeze y.a and y.c
std::vector<SymbolID> propsToFreeze;
propsToFreeze.push_back(*aHnd);
propsToFreeze.push_back(*cHnd);
propsToFreeze.push_back(*dHnd); // This is not in the map yet.
// Freeze while only create a singleton hidden class.
auto partlyFrozenSingleton =
HiddenClass::updatePropertyFlagsWithoutTransitions(
y,
runtime,
clearFlags,
setFlags,
llvh::ArrayRef<SymbolID>(propsToFreeze));
ASSERT_NE(*y, *partlyFrozenSingleton);
ASSERT_EQ(y->getNumProperties(), partlyFrozenSingleton->getNumProperties());
ASSERT_TRUE(partlyFrozenSingleton->isDictionary());
// Check each property
found = HiddenClass::findProperty(
partlyFrozenSingleton, runtime, *aHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(0u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_FALSE(desc.flags.configurable);
found = HiddenClass::findProperty(
partlyFrozenSingleton, runtime, *bHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(1u, desc.slot);
ASSERT_TRUE(desc.flags.writable);
ASSERT_TRUE(desc.flags.configurable);
found = HiddenClass::findProperty(
partlyFrozenSingleton, runtime, *cHnd, PropertyFlags::invalid(), desc);
ASSERT_TRUE(found);
ASSERT_EQ(2u, desc.slot);
ASSERT_FALSE(desc.flags.writable);
ASSERT_FALSE(desc.flags.configurable);
// We can still add another property to it.
auto addRes = HiddenClass::addProperty(
partlyFrozenSingleton,
runtime,
*dHnd,
PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(3u, addRes->second);
ASSERT_EQ(*addRes->first, *partlyFrozenSingleton);
ASSERT_EQ(addRes->first->getNumProperties(), 4);
}
TEST_F(HiddenClassTest, ForEachProperty) {
MutableHandle<HiddenClass> clazz{
runtime,
vmcast<HiddenClass>(
runtime.ignoreAllocationFailure(HiddenClass::createRoot(runtime)))};
auto aHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"a"));
auto bHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"b"));
{
// clazz.a
auto addRes = HiddenClass::addProperty(
clazz, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(0u, addRes->second);
clazz = *addRes->first;
}
{
// clazz.b
auto addRes = HiddenClass::addProperty(
clazz, runtime, *bHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
ASSERT_EQ(1u, addRes->second);
clazz = *addRes->first;
}
std::vector<std::pair<SymbolID, NamedPropertyDescriptor>> expectedProperties{
{aHnd.get(),
NamedPropertyDescriptor{
PropertyFlags::defaultNewNamedPropertyFlags(), 0}},
{bHnd.get(),
NamedPropertyDescriptor{
PropertyFlags::defaultNewNamedPropertyFlags(), 1}}};
std::vector<std::pair<SymbolID, NamedPropertyDescriptor>> properties;
HiddenClass::forEachProperty(
clazz, runtime, [&properties](SymbolID id, NamedPropertyDescriptor desc) {
properties.emplace_back(id, desc);
});
EXPECT_EQ(expectedProperties, properties);
std::vector<std::pair<SymbolID, NamedPropertyDescriptor>> propertiesNoAlloc;
HiddenClass::forEachPropertyNoAlloc(
clazz.get(),
runtime,
[&propertiesNoAlloc](SymbolID id, NamedPropertyDescriptor desc) {
propertiesNoAlloc.emplace_back(id, desc);
});
EXPECT_EQ(expectedProperties, propertiesNoAlloc);
}
TEST_F(HiddenClassTest, ReservedSlots) {
auto aHnd = *runtime.getIdentifierTable().getSymbolHandle(
runtime, createUTF16Ref(u"a"));
for (unsigned i = 0; i <= InternalProperty::NumAnonymousInternalProperties;
++i) {
Handle<HiddenClass> clazz =
runtime.getHiddenClassForPrototype(*runtime.getGlobal(), i);
EXPECT_FALSE(clazz->isDictionary());
auto addRes = HiddenClass::addProperty(
clazz, runtime, *aHnd, PropertyFlags::defaultNewNamedPropertyFlags());
ASSERT_RETURNED(addRes);
EXPECT_EQ(i, addRes->second);
}
}
} // namespace