Commit 0377bccd authored by bmeurer@chromium.org's avatar bmeurer@chromium.org

Fix and cleanup Map::GeneralizeRepresentation().

TEST=mjsunit/regress/regress-365172-[1-3],mjsunit/field-type-tracking
R=svenpanne@chromium.org

Review URL: https://codereview.chromium.org/259993004

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@21006 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent 629725d4
......@@ -2425,43 +2425,6 @@ Map* Map::FindRootMap() {
}
// Returns NULL if the updated map is incompatible.
Map* Map::FindUpdatedMap(int verbatim,
int length,
DescriptorArray* descriptors) {
DisallowHeapAllocation no_allocation;
// This can only be called on roots of transition trees.
ASSERT(GetBackPointer()->IsUndefined());
Map* current = this;
for (int i = verbatim; i < length; i++) {
if (!current->HasTransitionArray()) break;
Name* name = descriptors->GetKey(i);
TransitionArray* transitions = current->transitions();
int transition = transitions->Search(name);
if (transition == TransitionArray::kNotFound) break;
current = transitions->GetTarget(transition);
PropertyDetails details = descriptors->GetDetails(i);
PropertyDetails target_details =
current->instance_descriptors()->GetDetails(i);
if (details.attributes() != target_details.attributes()) return NULL;
if (details.type() == CALLBACKS) {
if (target_details.type() != CALLBACKS) return NULL;
if (descriptors->GetValue(i) !=
current->instance_descriptors()->GetValue(i)) {
return NULL;
}
} else if (target_details.type() == CALLBACKS) {
return NULL;
}
}
return current;
}
Map* Map::FindLastMatchMap(int verbatim,
int length,
DescriptorArray* descriptors) {
......@@ -2552,13 +2515,10 @@ void Map::GeneralizeFieldType(Handle<Map> map,
int modify_index,
Handle<HeapType> new_field_type) {
Isolate* isolate = map->GetIsolate();
Handle<Map> field_owner(map->FindFieldOwner(modify_index), isolate);
Handle<DescriptorArray> descriptors(
field_owner->instance_descriptors(), isolate);
// Check if we actually need to generalize the field type at all.
Handle<HeapType> old_field_type(
descriptors->GetFieldType(modify_index), isolate);
map->instance_descriptors()->GetFieldType(modify_index), isolate);
if (new_field_type->NowIs(old_field_type)) {
ASSERT(Map::GeneralizeFieldType(old_field_type,
new_field_type,
......@@ -2566,6 +2526,12 @@ void Map::GeneralizeFieldType(Handle<Map> map,
return;
}
// Determine the field owner.
Handle<Map> field_owner(map->FindFieldOwner(modify_index), isolate);
Handle<DescriptorArray> descriptors(
field_owner->instance_descriptors(), isolate);
ASSERT_EQ(*old_field_type, descriptors->GetFieldType(modify_index));
// Determine the generalized new field type.
new_field_type = Map::GeneralizeFieldType(
old_field_type, new_field_type, isolate);
......@@ -2598,23 +2564,28 @@ void Map::GeneralizeFieldType(Handle<Map> map,
// (partial) version of the type in the transition tree.
// To do this, on each rewrite:
// - Search the root of the transition tree using FindRootMap.
// - Find |updated|, the newest matching version of this map using
// FindUpdatedMap. This uses the keys in the own map's descriptor array to
// walk the transition tree.
// - Merge/generalize the descriptor array of the current map and |updated|.
// - Generalize the |modify_index| descriptor using |new_representation|.
// - Walk the tree again starting from the root towards |updated|. Stop at
// - Find |target_map|, the newest matching version of this map using the keys
// in the |old_map|'s descriptor array to walk the transition tree.
// - Merge/generalize the descriptor array of the |old_map| and |target_map|.
// - Generalize the |modify_index| descriptor using |new_representation| and
// |new_field_type|.
// - Walk the tree again starting from the root towards |target_map|. Stop at
// |split_map|, the first map who's descriptor array does not match the merged
// descriptor array.
// - If |updated| == |split_map|, |updated| is in the expected state. Return it.
// - Otherwise, invalidate the outdated transition target from |updated|, and
// - If |target_map| == |split_map|, |target_map| is in the expected state.
// Return it.
// - Otherwise, invalidate the outdated transition target from |target_map|, and
// replace its transition tree with a new branch for the updated descriptors.
Handle<Map> Map::GeneralizeRepresentation(Handle<Map> old_map,
int modify_index,
Representation new_representation,
Handle<HeapType> new_field_type,
StoreMode store_mode) {
Handle<DescriptorArray> old_descriptors(old_map->instance_descriptors());
Isolate* isolate = old_map->GetIsolate();
Handle<DescriptorArray> old_descriptors(
old_map->instance_descriptors(), isolate);
int old_nof = old_map->NumberOfOwnDescriptors();
PropertyDetails old_details = old_descriptors->GetDetails(modify_index);
Representation old_representation = old_details.representation();
......@@ -2641,84 +2612,239 @@ Handle<Map> Map::GeneralizeRepresentation(Handle<Map> old_map,
return old_map;
}
if (new_representation.Equals(old_representation) &&
old_details.type() == FIELD) {
Map::GeneralizeFieldType(old_map, modify_index, new_field_type);
return old_map;
}
Handle<Map> root_map(old_map->FindRootMap());
// Check the state of the root map.
Handle<Map> root_map(old_map->FindRootMap(), isolate);
if (!old_map->EquivalentToForTransition(*root_map)) {
return CopyGeneralizeAllRepresentations(old_map, modify_index, store_mode,
old_details.attributes(), "not equivalent");
}
int verbatim = root_map->NumberOfOwnDescriptors();
if (store_mode != ALLOW_AS_CONSTANT && modify_index < verbatim) {
int root_nof = root_map->NumberOfOwnDescriptors();
if (modify_index < root_nof) {
PropertyDetails old_details = old_descriptors->GetDetails(modify_index);
if ((old_details.type() != FIELD && store_mode == FORCE_FIELD) ||
(old_details.type() == FIELD &&
(!new_field_type->NowIs(old_descriptors->GetFieldType(modify_index)) ||
!new_representation.fits_into(old_details.representation())))) {
return CopyGeneralizeAllRepresentations(old_map, modify_index, store_mode,
old_details.attributes(), "root modification");
}
}
int descriptors = old_map->NumberOfOwnDescriptors();
Map* raw_updated = root_map->FindUpdatedMap(
verbatim, descriptors, *old_descriptors);
if (raw_updated == NULL) {
return CopyGeneralizeAllRepresentations(old_map, modify_index, store_mode,
Handle<Map> target_map = root_map;
for (int i = root_nof; i < old_nof; ++i) {
int j = target_map->SearchTransition(old_descriptors->GetKey(i));
if (j == TransitionArray::kNotFound) break;
Handle<Map> tmp_map(target_map->GetTransition(j), isolate);
Handle<DescriptorArray> tmp_descriptors = handle(
tmp_map->instance_descriptors(), isolate);
// Check if target map is incompatible.
PropertyDetails old_details = old_descriptors->GetDetails(i);
PropertyDetails tmp_details = tmp_descriptors->GetDetails(i);
PropertyType old_type = old_details.type();
PropertyType tmp_type = tmp_details.type();
if (tmp_details.attributes() != old_details.attributes() ||
((tmp_type == CALLBACKS || old_type == CALLBACKS) &&
(tmp_type != old_type ||
tmp_descriptors->GetValue(i) != old_descriptors->GetValue(i)))) {
return CopyGeneralizeAllRepresentations(
old_map, modify_index, store_mode,
old_details.attributes(), "incompatible");
}
Representation old_representation = old_details.representation();
Representation tmp_representation = tmp_details.representation();
if (!old_representation.fits_into(tmp_representation) ||
(!new_representation.fits_into(tmp_representation) &&
modify_index == i)) {
break;
}
if (tmp_type == FIELD) {
// Generalize the field type as necessary.
Handle<HeapType> old_field_type = (old_type == FIELD)
? handle(old_descriptors->GetFieldType(i), isolate)
: old_descriptors->GetValue(i)->OptimalType(
isolate, tmp_representation);
if (modify_index == i) {
old_field_type = GeneralizeFieldType(
new_field_type, old_field_type, isolate);
}
GeneralizeFieldType(tmp_map, i, old_field_type);
} else if (tmp_type == CONSTANT) {
if (old_type != CONSTANT ||
old_descriptors->GetConstant(i) != tmp_descriptors->GetConstant(i)) {
break;
}
} else {
ASSERT_EQ(tmp_type, old_type);
ASSERT_EQ(tmp_descriptors->GetValue(i), old_descriptors->GetValue(i));
}
target_map = tmp_map;
}
// Directly change the map if the target map is more general.
Handle<DescriptorArray> target_descriptors(
target_map->instance_descriptors(), isolate);
int target_nof = target_map->NumberOfOwnDescriptors();
if (target_nof == old_nof &&
(store_mode != FORCE_FIELD ||
target_descriptors->GetDetails(modify_index).type() == FIELD)) {
ASSERT(modify_index < target_nof);
ASSERT(new_representation.fits_into(
target_descriptors->GetDetails(modify_index).representation()));
ASSERT(target_descriptors->GetDetails(modify_index).type() != FIELD ||
new_field_type->NowIs(
target_descriptors->GetFieldType(modify_index)));
return target_map;
}
// Find the last compatible target map in the transition tree.
for (int i = target_nof; i < old_nof; ++i) {
int j = target_map->SearchTransition(old_descriptors->GetKey(i));
if (j == TransitionArray::kNotFound) break;
Handle<Map> tmp_map(target_map->GetTransition(j), isolate);
Handle<DescriptorArray> tmp_descriptors(
tmp_map->instance_descriptors(), isolate);
// Check if target map is compatible.
PropertyDetails old_details = old_descriptors->GetDetails(i);
PropertyDetails tmp_details = tmp_descriptors->GetDetails(i);
if (tmp_details.attributes() != old_details.attributes() ||
((tmp_details.type() == CALLBACKS || old_details.type() == CALLBACKS) &&
(tmp_details.type() != old_details.type() ||
tmp_descriptors->GetValue(i) != old_descriptors->GetValue(i)))) {
return CopyGeneralizeAllRepresentations(
old_map, modify_index, store_mode,
old_details.attributes(), "incompatible");
}
target_map = tmp_map;
}
target_nof = target_map->NumberOfOwnDescriptors();
target_descriptors = handle(target_map->instance_descriptors(), isolate);
Handle<Map> updated(raw_updated);
Handle<DescriptorArray> updated_descriptors(updated->instance_descriptors());
// Allocate a new descriptor array large enough to hold the required
// descriptors, with minimally the exact same size as the old descriptor
// array.
int new_slack = Max(
old_nof, old_descriptors->number_of_descriptors()) - old_nof;
Handle<DescriptorArray> new_descriptors = DescriptorArray::Allocate(
isolate, old_nof, new_slack);
ASSERT(new_descriptors->length() > target_descriptors->length() ||
new_descriptors->NumberOfSlackDescriptors() > 0 ||
new_descriptors->number_of_descriptors() ==
old_descriptors->number_of_descriptors());
ASSERT(new_descriptors->number_of_descriptors() == old_nof);
// 0 -> |root_nof|
int current_offset = 0;
for (int i = 0; i < root_nof; ++i) {
PropertyDetails old_details = old_descriptors->GetDetails(i);
if (old_details.type() == FIELD) current_offset++;
Descriptor d(handle(old_descriptors->GetKey(i), isolate),
handle(old_descriptors->GetValue(i), isolate),
old_details);
new_descriptors->Set(i, &d);
}
int valid = updated->NumberOfOwnDescriptors();
// |root_nof| -> |target_nof|
for (int i = root_nof; i < target_nof; ++i) {
Handle<Name> target_key(target_descriptors->GetKey(i), isolate);
PropertyDetails old_details = old_descriptors->GetDetails(i);
PropertyDetails target_details = target_descriptors->GetDetails(i);
target_details = target_details.CopyWithRepresentation(
old_details.representation().generalize(
target_details.representation()));
if (modify_index == i) {
target_details = target_details.CopyWithRepresentation(
new_representation.generalize(target_details.representation()));
}
if (old_details.type() == FIELD ||
target_details.type() == FIELD ||
(modify_index == i && store_mode == FORCE_FIELD) ||
(target_descriptors->GetValue(i) != old_descriptors->GetValue(i))) {
Handle<HeapType> old_field_type = (old_details.type() == FIELD)
? handle(old_descriptors->GetFieldType(i), isolate)
: old_descriptors->GetValue(i)->OptimalType(
isolate, target_details.representation());
Handle<HeapType> target_field_type = (target_details.type() == FIELD)
? handle(target_descriptors->GetFieldType(i), isolate)
: target_descriptors->GetValue(i)->OptimalType(
isolate, target_details.representation());
target_field_type = GeneralizeFieldType(
target_field_type, old_field_type, isolate);
if (modify_index == i) {
target_field_type = GeneralizeFieldType(
target_field_type, new_field_type, isolate);
}
FieldDescriptor d(target_key,
current_offset++,
target_field_type,
target_details.attributes(),
target_details.representation());
new_descriptors->Set(i, &d);
} else {
ASSERT_NE(FIELD, target_details.type());
Descriptor d(target_key,
handle(target_descriptors->GetValue(i), isolate),
target_details);
new_descriptors->Set(i, &d);
}
}
// Directly change the map if the target map is more general. Ensure that the
// target type of the modify_index is a FIELD, unless we are migrating.
if (updated_descriptors->IsMoreGeneralThan(
verbatim, valid, descriptors, *old_descriptors) &&
(store_mode == ALLOW_AS_CONSTANT ||
updated_descriptors->GetDetails(modify_index).type() == FIELD)) {
Representation updated_representation =
updated_descriptors->GetDetails(modify_index).representation();
if (new_representation.fits_into(updated_representation)) return updated;
// |target_nof| -> |old_nof|
for (int i = target_nof; i < old_nof; ++i) {
PropertyDetails old_details = old_descriptors->GetDetails(i);
Handle<Name> old_key(old_descriptors->GetKey(i), isolate);
if (modify_index == i) {
old_details = old_details.CopyWithRepresentation(
new_representation.generalize(old_details.representation()));
}
if (old_details.type() == FIELD) {
Handle<HeapType> old_field_type(
old_descriptors->GetFieldType(i), isolate);
if (modify_index == i) {
old_field_type = GeneralizeFieldType(
old_field_type, new_field_type, isolate);
}
FieldDescriptor d(old_key,
current_offset++,
old_field_type,
old_details.attributes(),
old_details.representation());
new_descriptors->Set(i, &d);
} else {
ASSERT(old_details.type() == CONSTANT || old_details.type() == CALLBACKS);
if (modify_index == i && store_mode == FORCE_FIELD) {
FieldDescriptor d(old_key,
current_offset++,
GeneralizeFieldType(
old_descriptors->GetValue(i)->OptimalType(
isolate, old_details.representation()),
new_field_type, isolate),
old_details.attributes(),
old_details.representation());
new_descriptors->Set(i, &d);
} else {
ASSERT_NE(FIELD, old_details.type());
Descriptor d(old_key,
handle(old_descriptors->GetValue(i), isolate),
old_details);
new_descriptors->Set(i, &d);
}
}
}
Handle<DescriptorArray> new_descriptors = DescriptorArray::Merge(
updated, verbatim, valid, descriptors, modify_index,
store_mode, old_map);
ASSERT(store_mode == ALLOW_AS_CONSTANT ||
new_descriptors->GetDetails(modify_index).type() == FIELD);
new_descriptors->Sort();
Isolate* isolate = new_descriptors->GetIsolate();
old_representation =
new_descriptors->GetDetails(modify_index).representation();
Representation updated_representation =
new_representation.generalize(old_representation);
if (!updated_representation.Equals(old_representation)) {
new_descriptors->SetRepresentation(modify_index, updated_representation);
}
if (new_descriptors->GetDetails(modify_index).type() == FIELD) {
Handle<HeapType> field_type(
new_descriptors->GetFieldType(modify_index), isolate);
new_field_type = Map::GeneralizeFieldType(
field_type, new_field_type, isolate);
new_descriptors->SetValue(modify_index, *new_field_type);
}
ASSERT(store_mode != FORCE_FIELD ||
new_descriptors->GetDetails(modify_index).type() == FIELD);
Handle<Map> split_map(root_map->FindLastMatchMap(
verbatim, descriptors, *new_descriptors));
root_nof, old_nof, *new_descriptors), isolate);
int split_nof = split_map->NumberOfOwnDescriptors();
ASSERT_NE(old_nof, split_nof);
int split_descriptors = split_map->NumberOfOwnDescriptors();
// This is shadowed by |updated_descriptors| being more general than
// |old_descriptors|.
ASSERT(descriptors != split_descriptors);
int descriptor = split_descriptors;
split_map->DeprecateTarget(
old_descriptors->GetKey(descriptor), *new_descriptors);
old_descriptors->GetKey(split_nof), *new_descriptors);
if (FLAG_trace_generalization) {
PropertyDetails old_details = old_descriptors->GetDetails(modify_index);
......@@ -2732,7 +2858,7 @@ Handle<Map> Map::GeneralizeRepresentation(Handle<Map> old_map,
: HeapType::Constant(handle(new_descriptors->GetValue(modify_index),
isolate), isolate);
old_map->PrintGeneralization(
stdout, "", modify_index, descriptor, descriptors,
stdout, "", modify_index, split_nof, old_nof,
old_details.type() == CONSTANT && store_mode == FORCE_FIELD,
old_details.representation(), new_details.representation(),
*old_field_type, *new_field_type);
......@@ -2740,10 +2866,9 @@ Handle<Map> Map::GeneralizeRepresentation(Handle<Map> old_map,
// Add missing transitions.
Handle<Map> new_map = split_map;
for (; descriptor < descriptors; descriptor++) {
new_map = CopyInstallDescriptors(new_map, descriptor, new_descriptors);
for (int i = split_nof; i < old_nof; ++i) {
new_map = CopyInstallDescriptors(new_map, i, new_descriptors);
}
new_map->set_owns_descriptors(true);
return new_map;
}
......@@ -8397,150 +8522,6 @@ void DescriptorArray::CopyFrom(int index,
}
// Creates a new descriptor array by merging the descriptor array of |right_map|
// into the (at least partly) updated descriptor array of |left_map|.
// The method merges two descriptor array in three parts. Both descriptor arrays
// are identical up to |verbatim|. They also overlap in keys up to |valid|.
// Between |verbatim| and |valid|, the resulting descriptor type as well as the
// representation are generalized from both |left_map| and |right_map|. Beyond
// |valid|, the descriptors are copied verbatim from |right_map| up to
// |new_size|.
// In case of incompatible types, the type and representation of |right_map| is
// used.
Handle<DescriptorArray> DescriptorArray::Merge(Handle<Map> left_map,
int verbatim,
int valid,
int new_size,
int modify_index,
StoreMode store_mode,
Handle<Map> right_map) {
ASSERT(verbatim <= valid);
ASSERT(valid <= new_size);
// Allocate a new descriptor array large enough to hold the required
// descriptors, with minimally the exact same size as this descriptor array.
Isolate* isolate = left_map->GetIsolate();
Handle<DescriptorArray> left(left_map->instance_descriptors());
Handle<DescriptorArray> right(right_map->instance_descriptors());
Handle<DescriptorArray> result = DescriptorArray::Allocate(
isolate,
new_size,
Max(new_size, right->number_of_descriptors()) - new_size);
ASSERT(result->length() > left->length() ||
result->NumberOfSlackDescriptors() > 0 ||
result->number_of_descriptors() == right->number_of_descriptors());
ASSERT(result->number_of_descriptors() == new_size);
int descriptor;
// 0 -> |verbatim|
int current_offset = 0;
for (descriptor = 0; descriptor < verbatim; descriptor++) {
if (left->GetDetails(descriptor).type() == FIELD) current_offset++;
Descriptor d(handle(right->GetKey(descriptor)),
handle(right->GetValue(descriptor), right->GetIsolate()),
right->GetDetails(descriptor));
result->Set(descriptor, &d);
}
// |verbatim| -> |valid|
for (; descriptor < valid; descriptor++) {
PropertyDetails left_details = left->GetDetails(descriptor);
PropertyDetails right_details = right->GetDetails(descriptor);
if (left_details.type() == FIELD || right_details.type() == FIELD ||
(store_mode == FORCE_FIELD && descriptor == modify_index) ||
(left_details.type() == CONSTANT &&
right_details.type() == CONSTANT &&
left->GetValue(descriptor) != right->GetValue(descriptor))) {
ASSERT(left_details.type() == CONSTANT || left_details.type() == FIELD);
ASSERT(right_details.type() == CONSTANT || right_details.type() == FIELD);
Representation representation = left_details.representation().generalize(
right_details.representation());
Handle<HeapType> left_type = (left_details.type() == FIELD)
? handle(left->GetFieldType(descriptor), isolate)
: left->GetValue(descriptor)->OptimalType(isolate, representation);
Handle<HeapType> right_type = (right_details.type() == FIELD)
? handle(right->GetFieldType(descriptor), isolate)
: right->GetValue(descriptor)->OptimalType(isolate, representation);
Handle<HeapType> field_type = Map::GeneralizeFieldType(
left_type, right_type, isolate);
FieldDescriptor d(handle(left->GetKey(descriptor), isolate),
current_offset++,
field_type,
right_details.attributes(),
representation);
result->Set(descriptor, &d);
} else {
Descriptor d(handle(right->GetKey(descriptor), isolate),
handle(right->GetValue(descriptor), isolate),
right_details);
result->Set(descriptor, &d);
}
}
// |valid| -> |new_size|
for (; descriptor < new_size; descriptor++) {
PropertyDetails right_details = right->GetDetails(descriptor);
if (right_details.type() == FIELD) {
FieldDescriptor d(handle(right->GetKey(descriptor), isolate),
current_offset++,
handle(right->GetFieldType(descriptor), isolate),
right_details.attributes(),
right_details.representation());
result->Set(descriptor, &d);
} else if (store_mode == FORCE_FIELD && descriptor == modify_index) {
ASSERT_EQ(CONSTANT, right_details.type());
Representation field_representation = right_details.representation();
Handle<HeapType> field_type = right->GetValue(descriptor)->OptimalType(
isolate, field_representation);
FieldDescriptor d(handle(right->GetKey(descriptor), isolate),
current_offset++,
field_type,
right_details.attributes(),
field_representation);
result->Set(descriptor, &d);
} else {
Descriptor d(handle(right->GetKey(descriptor), isolate),
handle(right->GetValue(descriptor), isolate),
right_details);
result->Set(descriptor, &d);
}
}
result->Sort();
return result;
}
// Checks whether a merge of |other| into |this| would return a copy of |this|.
bool DescriptorArray::IsMoreGeneralThan(int verbatim,
int valid,
int new_size,
DescriptorArray* other) {
ASSERT(verbatim <= valid);
ASSERT(valid <= new_size);
if (valid != new_size) return false;
for (int descriptor = verbatim; descriptor < valid; descriptor++) {
PropertyDetails details = GetDetails(descriptor);
PropertyDetails other_details = other->GetDetails(descriptor);
if (!other_details.representation().fits_into(details.representation())) {
return false;
}
if (details.type() == CONSTANT) {
if (other_details.type() != CONSTANT) return false;
if (GetValue(descriptor) != other->GetValue(descriptor)) return false;
} else if (details.type() == FIELD && other_details.type() == FIELD) {
if (!other->GetFieldType(descriptor)->NowIs(GetFieldType(descriptor))) {
return false;
}
}
}
return true;
}
// We need the whiteness witness since sort will reshuffle the entries in the
// descriptor array. If the descriptor array were to be black, the shuffling
// would move a slot that was already recorded as pointing into an evacuation
......
......@@ -3441,20 +3441,6 @@ class DescriptorArray: public FixedArray {
// array.
inline void Append(Descriptor* desc);
static Handle<DescriptorArray> Merge(Handle<Map> left_map,
int verbatim,
int valid,
int new_size,
int modify_index,
StoreMode store_mode,
Handle<Map> right_map)
V8_WARN_UNUSED_RESULT;
bool IsMoreGeneralThan(int verbatim,
int valid,
int new_size,
DescriptorArray* other);
static Handle<DescriptorArray> CopyUpTo(Handle<DescriptorArray> desc,
int enumeration_index,
int slack = 0);
......@@ -6733,7 +6719,6 @@ class Map: public HeapObject {
void DeprecateTransitionTree();
void DeprecateTarget(Name* key, DescriptorArray* new_descriptors);
Map* FindUpdatedMap(int verbatim, int length, DescriptorArray* descriptors);
Map* FindLastMatchMap(int verbatim, int length, DescriptorArray* descriptors);
void UpdateDescriptor(int descriptor_number, Descriptor* desc);
......
......@@ -66,6 +66,7 @@ class Descriptor BASE_EMBEDDED {
details_(attributes, type, representation, field_index) { }
friend class DescriptorArray;
friend class Map;
};
......
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