Commit afe9020f authored by Frank Emrich's avatar Frank Emrich Committed by Commit Bot

[dict-proto] TF support for constants in dictionary mode protos, pt. 2

This CL is part of a  series that implements Turbofan support for
property accesses satisfying the following conditions:
1. The holder is a dictionary mode object.
2. The holder is a prototype.
3. The access is a load.

This feature will only be enabled if the build flag
v8_dict_property_const_tracking is set.

This particular CL implements support for the case that the property
in question is a data property, meaning that the given
PropertyAccessInfo has kind kDataDictionaryProtoConstant.
Support for accessor properties is added in a separated CL.

Bug: v8:11248
Change-Id: I8794127d08c3d3aed6ec2a3eb19c4c82bdf2d1df
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2718229
Commit-Queue: Frank Emrich <emrich@google.com>
Reviewed-by: 's avatarGeorg Neis <neis@chromium.org>
Reviewed-by: 's avatarMarja Hölttä <marja@chromium.org>
Cr-Commit-Position: refs/heads/master@{#73603}
parent 8e86f221
......@@ -67,6 +67,11 @@ static V8_INLINE bool CheckForName(Isolate* isolate, Handle<Name> name,
// If true, *object_offset contains offset of object field.
bool Accessors::IsJSObjectFieldAccessor(Isolate* isolate, Handle<Map> map,
Handle<Name> name, FieldIndex* index) {
if (map->is_dictionary_map()) {
// There are not descriptors in a dictionary mode map.
return false;
}
switch (map->instance_type()) {
case JS_ARRAY_TYPE:
return CheckForName(isolate, name, isolate->factory()->length_string(),
......
......@@ -27,17 +27,27 @@ namespace compiler {
namespace {
bool CanInlinePropertyAccess(Handle<Map> map) {
bool CanInlinePropertyAccess(Handle<Map> map, AccessMode access_mode) {
// We can inline property access to prototypes of all primitives, except
// the special Oddball ones that have no wrapper counterparts (i.e. Null,
// Undefined and TheHole).
// We can only inline accesses to dictionary mode holders if the access is a
// load and the holder is a prototype. The latter ensures a 1:1
// relationship between the map and the object (and therefore the property
// dictionary).
STATIC_ASSERT(ODDBALL_TYPE == LAST_PRIMITIVE_HEAP_OBJECT_TYPE);
if (map->IsBooleanMap()) return true;
if (map->instance_type() < LAST_PRIMITIVE_HEAP_OBJECT_TYPE) return true;
return map->IsJSObjectMap() && !map->is_dictionary_map() &&
!map->has_named_interceptor() &&
// TODO(verwaest): Allowlist contexts to which we have access.
!map->is_access_check_needed();
if (map->IsJSObjectMap()) {
if (map->is_dictionary_map()) {
if (!V8_DICT_PROPERTY_CONST_TRACKING_BOOL) return false;
return access_mode == AccessMode::kLoad && map->is_prototype_map();
}
return !map->has_named_interceptor() &&
// TODO(verwaest): Allowlist contexts to which we have access.
!map->is_access_check_needed();
}
return false;
}
#ifdef DEBUG
......@@ -146,9 +156,9 @@ PropertyAccessInfo PropertyAccessInfo::StringLength(Zone* zone,
// static
PropertyAccessInfo PropertyAccessInfo::DictionaryProtoDataConstant(
Zone* zone, Handle<Map> receiver_map, Handle<JSObject> holder,
InternalIndex dictionary_index) {
InternalIndex dictionary_index, Handle<Name> name) {
return PropertyAccessInfo(zone, kDictionaryProtoDataConstant, holder,
{{receiver_map}, zone}, dictionary_index);
{{receiver_map}, zone}, dictionary_index, name);
}
// static
......@@ -227,14 +237,15 @@ PropertyAccessInfo::PropertyAccessInfo(
PropertyAccessInfo::PropertyAccessInfo(
Zone* zone, Kind kind, MaybeHandle<JSObject> holder,
ZoneVector<Handle<Map>>&& lookup_start_object_maps,
InternalIndex dictionary_index)
InternalIndex dictionary_index, Handle<Name> name)
: kind_(kind),
lookup_start_object_maps_(lookup_start_object_maps),
holder_(holder),
unrecorded_dependencies_(zone),
field_representation_(Representation::None()),
field_type_(Type::Any()),
dictionary_index_(dictionary_index) {}
dictionary_index_(dictionary_index),
name_{name} {}
MinimorphicLoadPropertyAccessInfo::MinimorphicLoadPropertyAccessInfo(
Kind kind, int offset, bool is_inobject,
......@@ -323,6 +334,18 @@ bool PropertyAccessInfo::Merge(PropertyAccessInfo const* that,
return false;
}
case kDictionaryProtoDataConstant: {
DCHECK_EQ(AccessMode::kLoad, access_mode);
if (this->dictionary_index_ == that->dictionary_index_) {
this->lookup_start_object_maps_.insert(
this->lookup_start_object_maps_.end(),
that->lookup_start_object_maps_.begin(),
that->lookup_start_object_maps_.end());
return true;
}
return false;
}
case kNotFound:
case kStringLength: {
DCHECK(this->unrecorded_dependencies_.empty());
......@@ -336,7 +359,6 @@ bool PropertyAccessInfo::Merge(PropertyAccessInfo const* that,
case kModuleExport:
return false;
case kDictionaryProtoDataConstant:
case kDictionaryProtoAccessorConstant:
// TODO(v8:11248) Dealt with in follow-up CLs.
UNREACHABLE();
......@@ -559,6 +581,28 @@ PropertyAccessInfo AccessInfoFactory::ComputeAccessorDescriptorAccessInfo(
accessor, holder);
}
PropertyAccessInfo AccessInfoFactory::ComputeDictionaryProtoAccessInfo(
Handle<Map> receiver_map, Handle<Name> name, Handle<JSObject> holder,
InternalIndex dictionary_index, AccessMode access_mode,
PropertyDetails details) const {
CHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
DCHECK(holder->map().is_prototype_map());
DCHECK_EQ(access_mode, AccessMode::kLoad);
// We can only inline accesses to constant properties.
if (details.constness() != PropertyConstness::kConst) {
return PropertyAccessInfo::Invalid(zone());
}
if (details.kind() == PropertyKind::kData) {
return PropertyAccessInfo::DictionaryProtoDataConstant(
zone(), receiver_map, holder, dictionary_index, name);
}
// TODO(v8:11248) Support for accessors is implemented a in follow-up CL.
return PropertyAccessInfo::Invalid(zone());
}
MinimorphicLoadPropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
MinimorphicLoadPropertyAccessFeedback const& feedback) const {
DCHECK(feedback.handler()->IsSmi());
......@@ -573,6 +617,50 @@ MinimorphicLoadPropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
field_rep, field_type);
}
bool AccessInfoFactory::TryLoadPropertyDetails(
Handle<Map> map, MaybeHandle<JSObject> maybe_holder, Handle<Name> name,
InternalIndex* index_out, PropertyDetails* details_out) const {
if (map->is_dictionary_map()) {
DCHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
DCHECK(map->is_prototype_map());
DisallowGarbageCollection no_gc;
if (maybe_holder.is_null()) {
// TODO(v8:11457) In this situation, we have a dictionary mode prototype
// as a receiver. Consider other means of obtaining the holder in this
// situation.
// Without the holder, we can't get the property details.
return false;
}
Handle<JSObject> holder = maybe_holder.ToHandleChecked();
if (V8_DICT_MODE_PROTOTYPES_BOOL) {
SwissNameDictionary dict = holder->property_dictionary_swiss();
*index_out = dict.FindEntry(isolate(), name);
if (index_out->is_found()) {
*details_out = dict.DetailsAt(*index_out);
}
} else {
NameDictionary dict = holder->property_dictionary();
*index_out = dict.FindEntry(isolate(), name);
if (index_out->is_found()) {
*details_out = dict.DetailsAt(*index_out);
}
}
} else {
DescriptorArray descriptors = map->instance_descriptors(kAcquireLoad);
*index_out =
descriptors.Search(*name, *map, broker()->is_concurrent_inlining());
if (index_out->is_found()) {
*details_out = descriptors.GetDetails(*index_out);
}
}
return true;
}
PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
Handle<Map> map, Handle<Name> name, AccessMode access_mode) const {
CHECK(name->IsUniqueName());
......@@ -582,7 +670,7 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
}
// Check if it is safe to inline property access for the {map}.
if (!CanInlinePropertyAccess(map)) {
if (!CanInlinePropertyAccess(map, access_mode)) {
return PropertyAccessInfo::Invalid(zone());
}
......@@ -592,19 +680,25 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
if (!access_info.IsInvalid()) return access_info;
}
// Only relevant if V8_DICT_PROPERTY_CONST_TRACKING enabled.
bool dictionary_prototype_on_chain = false;
bool fast_mode_prototype_on_chain = false;
// Remember the receiver map. We use {map} as loop variable.
Handle<Map> receiver_map = map;
MaybeHandle<JSObject> holder;
while (true) {
// Lookup the named property on the {map}.
Handle<DescriptorArray> descriptors(map->instance_descriptors(kAcquireLoad),
isolate());
InternalIndex const number =
descriptors->Search(*name, *map, broker()->is_concurrent_inlining());
if (number.is_found()) {
PropertyDetails const details = descriptors->GetDetails(number);
PropertyDetails details = PropertyDetails::Empty();
InternalIndex index = InternalIndex::NotFound();
if (!TryLoadPropertyDetails(map, holder, name, &index, &details)) {
return PropertyAccessInfo::Invalid(zone());
}
if (index.is_found()) {
if (access_mode == AccessMode::kStore ||
access_mode == AccessMode::kStoreInLiteral) {
DCHECK(!map->is_dictionary_map());
// Don't bother optimizing stores to read-only properties.
if (details.IsReadOnly()) {
return PropertyAccessInfo::Invalid(zone());
......@@ -617,9 +711,43 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
return LookupTransition(receiver_map, name, holder);
}
}
if (map->is_dictionary_map()) {
DCHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
if (fast_mode_prototype_on_chain) {
// TODO(v8:11248) While the work on dictionary mode prototypes is in
// progress, we may still see fast mode objects on the chain prior to
// reaching a dictionary mode prototype holding the property . Due to
// this only being an intermediate state, we don't stupport these kind
// of heterogenous prototype chains.
return PropertyAccessInfo::Invalid(zone());
}
// TryLoadPropertyDetails only succeeds if we know the holder.
return ComputeDictionaryProtoAccessInfo(receiver_map, name,
holder.ToHandleChecked(), index,
access_mode, details);
}
if (dictionary_prototype_on_chain) {
// If V8_DICT_PROPERTY_CONST_TRACKING_BOOL was disabled, then a
// dictionary prototype would have caused a bailout earlier.
DCHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
// TODO(v8:11248) We have a fast mode holder, but there was a dictionary
// mode prototype earlier on the chain. Note that seeing a fast mode
// prototype even though V8_DICT_PROPERTY_CONST_TRACKING is enabled
// should only be possible while the implementation of dictionary mode
// prototypes is work in progress. Eventually, enabling
// V8_DICT_PROPERTY_CONST_TRACKING will guarantee that all prototypes
// are always in dictionary mode, making this case unreachable. However,
// due to the complications of checking dictionary mode prototypes for
// modification, we don't attempt to support dictionary mode prototypes
// occuring before a fast mode holder on the chain.
return PropertyAccessInfo::Invalid(zone());
}
if (details.location() == kField) {
if (details.kind() == kData) {
return ComputeDataFieldAccessInfo(receiver_map, map, holder, number,
return ComputeDataFieldAccessInfo(receiver_map, map, holder, index,
access_mode);
} else {
DCHECK_EQ(kAccessor, details.kind());
......@@ -630,8 +758,9 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
DCHECK_EQ(kDescriptor, details.location());
DCHECK_EQ(kAccessor, details.kind());
return ComputeAccessorDescriptorAccessInfo(receiver_map, name, map,
holder, number, access_mode);
holder, index, access_mode);
}
UNREACHABLE();
}
......@@ -654,6 +783,17 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
return PropertyAccessInfo::Invalid(zone());
}
if (V8_DICT_PROPERTY_CONST_TRACKING_BOOL && !holder.is_null()) {
// At this point, we are past the first loop iteration.
DCHECK(holder.ToHandleChecked()->map().is_prototype_map());
DCHECK_NE(holder.ToHandleChecked()->map(), *receiver_map);
fast_mode_prototype_on_chain =
fast_mode_prototype_on_chain || !map->is_dictionary_map();
dictionary_prototype_on_chain =
dictionary_prototype_on_chain || map->is_dictionary_map();
}
// Walk up the prototype chain.
MapRef(broker(), map).SerializePrototype();
// Acquire synchronously the map's prototype's map to guarantee that every
......@@ -672,6 +812,15 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
handle(map->prototype().synchronized_map(), isolate());
DCHECK(map_prototype_map->IsJSObjectMap());
} else if (map->prototype().IsNull()) {
if (dictionary_prototype_on_chain) {
// TODO(v8:11248) See earlier comment about
// dictionary_prototype_on_chain. We don't support absent properties
// with dictionary mode prototypes on the chain, either. This is again
// just due to how we currently deal with dependencies for dictionary
// properties during finalization.
return PropertyAccessInfo::Invalid(zone());
}
// Store to property not found on the receiver or any prototype, we need
// to transition to a new data property.
// Implemented according to ES6 section 9.1.9 [[Set]] (P, V, Receiver)
......@@ -691,14 +840,14 @@ PropertyAccessInfo AccessInfoFactory::ComputePropertyAccessInfo(
map = map_prototype_map;
CHECK(!map->is_deprecated());
if (!CanInlinePropertyAccess(map)) {
if (!CanInlinePropertyAccess(map, access_mode)) {
return PropertyAccessInfo::Invalid(zone());
}
// Successful lookup on prototype chain needs to guarantee that all
// the prototypes up to the holder have stable maps. Let us make sure
// the prototype maps are stable here.
CHECK(map->is_stable());
// Successful lookup on prototype chain needs to guarantee that all the
// prototypes up to the holder have stable maps, except for dictionary-mode
// prototypes.
CHECK_IMPLIES(!map->is_dictionary_map(), map->is_stable());
}
UNREACHABLE();
}
......
......@@ -102,7 +102,7 @@ class PropertyAccessInfo final {
static PropertyAccessInfo Invalid(Zone* zone);
static PropertyAccessInfo DictionaryProtoDataConstant(
Zone* zone, Handle<Map> receiver_map, Handle<JSObject> holder,
InternalIndex dict_index);
InternalIndex dict_index, Handle<Name> name);
static PropertyAccessInfo DictionaryProtoAccessorConstant(
Zone* zone, Handle<Map> receiver_map, MaybeHandle<JSObject> holder,
Handle<Object> constant);
......@@ -173,6 +173,11 @@ class PropertyAccessInfo final {
return dictionary_index_;
}
Handle<Name> name() const {
DCHECK(HasDictionaryHolder());
return name_.ToHandleChecked();
}
private:
explicit PropertyAccessInfo(Zone* zone);
PropertyAccessInfo(Zone* zone, Kind kind, MaybeHandle<JSObject> holder,
......@@ -188,7 +193,7 @@ class PropertyAccessInfo final {
ZoneVector<CompilationDependency const*>&& dependencies);
PropertyAccessInfo(Zone* zone, Kind kind, MaybeHandle<JSObject> holder,
ZoneVector<Handle<Map>>&& lookup_start_object_maps,
InternalIndex dictionary_index);
InternalIndex dictionary_index, Handle<Name> name);
// Members used for fast and dictionary mode holders:
Kind kind_;
......@@ -207,6 +212,7 @@ class PropertyAccessInfo final {
// Members only used for dictionary mode holders:
InternalIndex dictionary_index_;
MaybeHandle<Name> name_;
};
// This class encapsulates information required to generate load properties
......@@ -255,6 +261,11 @@ class AccessInfoFactory final {
Handle<Name> name,
AccessMode access_mode) const;
PropertyAccessInfo ComputeDictionaryProtoAccessInfo(
Handle<Map> receiver_map, Handle<Name> name, Handle<JSObject> holder,
InternalIndex dict_index, AccessMode access_mode,
PropertyDetails details) const;
MinimorphicLoadPropertyAccessInfo ComputePropertyAccessInfo(
MinimorphicLoadPropertyAccessFeedback const& feedback) const;
......@@ -298,6 +309,10 @@ class AccessInfoFactory final {
AccessMode access_mode,
ZoneVector<PropertyAccessInfo>* result) const;
bool TryLoadPropertyDetails(Handle<Map> map, MaybeHandle<JSObject> holder,
Handle<Name> name, InternalIndex* index_out,
PropertyDetails* details_out) const;
CompilationDependencies* dependencies() const { return dependencies_; }
JSHeapBroker* broker() const { return broker_; }
Isolate* isolate() const;
......
......@@ -105,6 +105,105 @@ class StableMapDependency final : public CompilationDependency {
MapRef map_;
};
class ConstantInDictionaryPrototypeChainDependency final
: public CompilationDependency {
public:
explicit ConstantInDictionaryPrototypeChainDependency(
const MapRef receiver_map, const NameRef property_name,
const ObjectRef constant)
: receiver_map_(receiver_map),
property_name_{property_name},
constant_{constant} {
DCHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
}
// Checks that |constant_| is still the value of accessing |property_name_|
// starting at |receiver_map_|.
bool IsValid() const override { return !GetHolderIfValid().is_null(); }
void Install(const MaybeObjectHandle& code) const override {
SLOW_DCHECK(IsValid());
Isolate* isolate = receiver_map_.isolate();
Handle<JSObject> holder = GetHolderIfValid().ToHandleChecked();
Handle<Map> map = receiver_map_.object();
while (map->prototype() != *holder) {
map = handle(map->prototype().map(), isolate);
DCHECK(map->IsJSObjectMap()); // Due to IsValid holding.
DependentCode::InstallDependency(isolate, code, map,
DependentCode::kPrototypeCheckGroup);
}
DCHECK(map->prototype().map().IsJSObjectMap()); // Due to IsValid holding.
DependentCode::InstallDependency(isolate, code,
handle(map->prototype().map(), isolate),
DependentCode::kPrototypeCheckGroup);
}
private:
// If the dependency is still valid, returns holder of the constant. Otherwise
// returns null.
// TODO(neis) Currently, invoking IsValid and then Install duplicates the call
// to GetHolderIfValid. Instead, consider letting IsValid change the state
// (and store the holder), or merge IsValid and Install.
MaybeHandle<JSObject> GetHolderIfValid() const {
DisallowGarbageCollection no_gc;
Isolate* isolate = receiver_map_.isolate();
Handle<Object> holder;
HeapObject prototype = receiver_map_.object()->prototype();
enum class ValidationResult { kFoundCorrect, kFoundIncorrect, kNotFound };
auto try_load = [&](auto dictionary) -> ValidationResult {
InternalIndex entry =
dictionary.FindEntry(isolate, property_name_.object());
if (entry.is_not_found()) {
return ValidationResult::kNotFound;
}
PropertyDetails details = dictionary.DetailsAt(entry);
if (details.constness() != PropertyConstness::kConst) {
return ValidationResult::kFoundIncorrect;
}
Object value = dictionary.ValueAt(entry);
return value == *constant_.object() ? ValidationResult::kFoundCorrect
: ValidationResult::kFoundIncorrect;
};
while (prototype.IsJSObject()) {
// We only care about JSObjects because that's the only type of holder
// (and types of prototypes on the chain to the holder) that
// AccessInfoFactory::ComputePropertyAccessInfo allows.
JSObject object = JSObject::cast(prototype);
// We only support dictionary mode prototypes on the chain for this kind
// of dependency.
CHECK(!object.HasFastProperties());
ValidationResult result =
V8_DICT_MODE_PROTOTYPES_BOOL
? try_load(object.property_dictionary_swiss())
: try_load(object.property_dictionary());
if (result == ValidationResult::kFoundCorrect) {
return handle(object, isolate);
} else if (result == ValidationResult::kFoundIncorrect) {
return MaybeHandle<JSObject>();
}
// In case of kNotFound, continue walking up the chain.
prototype = object.map().prototype();
}
return MaybeHandle<JSObject>();
}
MapRef receiver_map_;
NameRef property_name_;
ObjectRef constant_;
};
class TransitionDependency final : public CompilationDependency {
public:
explicit TransitionDependency(const MapRef& map) : map_(map) {
......@@ -394,6 +493,7 @@ ObjectRef CompilationDependencies::DependOnPrototypeProperty(
}
void CompilationDependencies::DependOnStableMap(const MapRef& map) {
DCHECK(!map.is_dictionary_map());
DCHECK(!map.IsNeverSerializedHeapObject());
if (map.CanTransition()) {
RecordDependency(zone_->New<StableMapDependency>(map));
......@@ -402,6 +502,13 @@ void CompilationDependencies::DependOnStableMap(const MapRef& map) {
}
}
void CompilationDependencies::DependOnConstantInDictionaryPrototypeChain(
const MapRef& receiver_map, const NameRef& property_name,
const ObjectRef& constant) {
RecordDependency(zone_->New<ConstantInDictionaryPrototypeChainDependency>(
receiver_map, property_name, constant));
}
AllocationType CompilationDependencies::DependOnPretenureMode(
const AllocationSiteRef& site) {
DCHECK(!site.IsNeverSerializedHeapObject());
......
......@@ -45,6 +45,16 @@ class V8_EXPORT_PRIVATE CompilationDependencies : public ZoneObject {
// Record the assumption that {map} stays stable.
void DependOnStableMap(const MapRef& map);
// Depend on the fact that accessing property |property_name| from
// |receiver_map| yields the constant value |constant|, which is held by
// |holder|. Therefore, must be invalidated if |property_name| is added to any
// of the objects between receiver and |holder| on the prototype chain, b) any
// of the objects on the prototype chain up to |holder| change prototypes, or
// c) the value of |property_name| in |holder| changes.
void DependOnConstantInDictionaryPrototypeChain(const MapRef& receiver_map,
const NameRef& property_name,
const ObjectRef& constant);
// Return the pretenure mode of {site} and record the assumption that it does
// not change.
AllocationType DependOnPretenureMode(const AllocationSiteRef& site);
......
......@@ -347,6 +347,13 @@ class JSObjectRef : public JSReceiverRef {
Representation field_representation, FieldIndex index,
SerializationPolicy policy =
SerializationPolicy::kAssumeSerialized) const;
// Return the value of the dictionary property at {index} in the dictionary
// if {index} is known to be an own data property of the object.
ObjectRef GetOwnDictionaryProperty(
InternalIndex index, SerializationPolicy policy =
SerializationPolicy::kAssumeSerialized) const;
base::Optional<FixedArrayBaseRef> elements() const;
void SerializeElements();
void EnsureElementsTenured();
......
......@@ -7706,6 +7706,7 @@ Reduction JSCallReducer::ReduceRegExpPrototypeTest(Node* node) {
ai_exec.lookup_start_object_maps(), kStartAtPrototype,
JSObjectRef(broker(), holder));
} else {
// TODO(v8:11457) Support dictionary mode protoypes here.
return inference.NoChange();
}
......
......@@ -426,6 +426,9 @@ class JSObjectData : public JSReceiverData {
JSHeapBroker* broker, Representation representation,
FieldIndex field_index,
SerializationPolicy policy = SerializationPolicy::kAssumeSerialized);
ObjectData* GetOwnDictionaryProperty(JSHeapBroker* broker,
InternalIndex dict_index,
SerializationPolicy policy);
// This method is only used to assert our invariants.
bool cow_or_empty_elements_tenured() const;
......@@ -455,8 +458,9 @@ class JSObjectData : public JSReceiverData {
// (2) are known not to (possibly they don't exist at all).
// In case (2), the second pair component is nullptr.
// For simplicity, this may in theory overlap with inobject_fields_.
// The keys of the map are the property_index() values of the
// respective property FieldIndex'es.
// For fast mode objects, the keys of the map are the property_index() values
// of the respective property FieldIndex'es. For slow mode objects, the keys
// are the dictionary indicies.
ZoneUnorderedMap<int, ObjectData*> own_properties_;
};
......@@ -505,6 +509,14 @@ ObjectRef GetOwnFastDataPropertyFromHeap(JSHeapBroker* broker,
return ObjectRef(broker, constant);
}
ObjectRef GetOwnDictionaryPropertyFromHeap(JSHeapBroker* broker,
Handle<JSObject> receiver,
InternalIndex dict_index) {
Handle<Object> constant =
JSObject::DictionaryPropertyAt(receiver, dict_index);
return ObjectRef(broker, constant);
}
} // namespace
ObjectData* JSObjectData::GetOwnConstantElement(JSHeapBroker* broker,
......@@ -547,6 +559,25 @@ ObjectData* JSObjectData::GetOwnFastDataProperty(JSHeapBroker* broker,
return result;
}
ObjectData* JSObjectData::GetOwnDictionaryProperty(JSHeapBroker* broker,
InternalIndex dict_index,
SerializationPolicy policy) {
auto p = own_properties_.find(dict_index.as_int());
if (p != own_properties_.end()) return p->second;
if (policy == SerializationPolicy::kAssumeSerialized) {
TRACE_MISSING(broker, "knowledge about dictionary property with index "
<< dict_index.as_int() << " on " << this);
return nullptr;
}
ObjectRef property = GetOwnDictionaryPropertyFromHeap(
broker, Handle<JSObject>::cast(object()), dict_index);
ObjectData* result(property.data());
own_properties_.insert(std::make_pair(dict_index.as_int(), result));
return result;
}
class JSTypedArrayData : public JSObjectData {
public:
JSTypedArrayData(JSHeapBroker* broker, ObjectData** storage,
......@@ -4034,6 +4065,19 @@ base::Optional<ObjectRef> JSObjectRef::GetOwnFastDataProperty(
return ObjectRef(broker(), property);
}
ObjectRef JSObjectRef::GetOwnDictionaryProperty(
InternalIndex index, SerializationPolicy policy) const {
CHECK(index.is_found());
if (data_->should_access_heap()) {
return GetOwnDictionaryPropertyFromHeap(
broker(), Handle<JSObject>::cast(object()), index);
}
ObjectData* property =
data()->AsJSObject()->GetOwnDictionaryProperty(broker(), index, policy);
CHECK_NE(property, nullptr);
return ObjectRef(broker(), property);
}
ObjectRef JSArrayRef::GetBoilerplateLength() const {
// Safe to read concurrently because:
// - boilerplates are immutable after initialization.
......
......@@ -424,7 +424,9 @@ Reduction JSNativeContextSpecialization::ReduceJSInstanceOf(Node* node) {
AccessMode::kLoad);
}
if (access_info.IsInvalid()) return NoChange();
// TODO(v8:11457) Support dictionary mode holders here.
if (access_info.IsInvalid() || access_info.HasDictionaryHolder())
return NoChange();
access_info.RecordDependencies(dependencies());
PropertyAccessBuilder access_builder(jsgraph(), broker(), dependencies());
......@@ -554,7 +556,9 @@ JSNativeContextSpecialization::InferHasInPrototypeChain(
break;
}
map = map.prototype().map();
if (!map.is_stable()) return kMayBeInPrototypeChain;
// TODO(v8:11457) Support dictionary mode protoypes here.
if (!map.is_stable() || map.is_dictionary_map())
return kMayBeInPrototypeChain;
if (map.oddball_type() == OddballType::kNull) {
all = false;
break;
......@@ -741,7 +745,10 @@ Reduction JSNativeContextSpecialization::ReduceJSResolvePromise(Node* node) {
PropertyAccessInfo access_info =
access_info_factory.FinalizePropertyAccessInfosAsOne(access_infos,
AccessMode::kLoad);
if (access_info.IsInvalid()) return inference.NoChange();
// TODO(v8:11457) Support dictionary mode prototypes here.
if (access_info.IsInvalid() || access_info.HasDictionaryHolder())
return inference.NoChange();
// Only optimize when {resolution} definitely doesn't have a "then" property.
if (!access_info.IsNotFound()) return inference.NoChange();
......@@ -2341,7 +2348,8 @@ JSNativeContextSpecialization::BuildPropertyLoad(
ZoneVector<Node*>* if_exceptions, PropertyAccessInfo const& access_info) {
// Determine actual holder and perform prototype chain checks.
Handle<JSObject> holder;
if (access_info.holder().ToHandle(&holder)) {
if (access_info.holder().ToHandle(&holder) &&
!access_info.HasDictionaryHolder()) {
dependencies()->DependOnStablePrototypeChains(
access_info.lookup_start_object_maps(), kStartAtPrototype,
JSObjectRef(broker(), holder));
......@@ -2369,10 +2377,15 @@ JSNativeContextSpecialization::BuildPropertyLoad(
DCHECK_EQ(receiver, lookup_start_object);
value = graph()->NewNode(simplified()->StringLength(), receiver);
} else {
DCHECK(access_info.IsDataField() || access_info.IsFastDataConstant());
DCHECK(access_info.IsDataField() || access_info.IsFastDataConstant() ||
access_info.IsDictionaryProtoDataConstant());
PropertyAccessBuilder access_builder(jsgraph(), broker(), dependencies());
value = access_builder.BuildLoadDataField(
name, access_info, lookup_start_object, &effect, &control);
if (access_info.IsDictionaryProtoDataConstant()) {
value = access_builder.FoldLoadDictPrototypeConstant(access_info);
} else {
value = access_builder.BuildLoadDataField(
name, access_info, lookup_start_object, &effect, &control);
}
}
return ValueEffectControl(value, effect, control);
......@@ -2381,6 +2394,9 @@ JSNativeContextSpecialization::BuildPropertyLoad(
JSNativeContextSpecialization::ValueEffectControl
JSNativeContextSpecialization::BuildPropertyTest(
Node* effect, Node* control, PropertyAccessInfo const& access_info) {
// TODO(v8:11457) Support property tests for dictionary mode protoypes.
DCHECK(!access_info.HasDictionaryHolder());
// Determine actual holder and perform prototype chain checks.
Handle<JSObject> holder;
if (access_info.holder().ToHandle(&holder)) {
......
......@@ -148,7 +148,25 @@ MachineRepresentation PropertyAccessBuilder::ConvertRepresentation(
}
}
Node* PropertyAccessBuilder::TryBuildLoadConstantDataField(
Node* PropertyAccessBuilder::FoldLoadDictPrototypeConstant(
PropertyAccessInfo const& access_info) {
DCHECK(V8_DICT_PROPERTY_CONST_TRACKING_BOOL);
DCHECK(access_info.IsDictionaryProtoDataConstant());
JSObjectRef holder(broker(), access_info.holder().ToHandleChecked());
base::Optional<ObjectRef> value =
holder.GetOwnDictionaryProperty(access_info.dictionary_index());
for (const Handle<Map> map : access_info.lookup_start_object_maps()) {
dependencies()->DependOnConstantInDictionaryPrototypeChain(
MapRef{broker(), map}, NameRef{broker(), access_info.name()},
value.value());
}
return jsgraph()->Constant(value.value());
}
Node* PropertyAccessBuilder::TryFoldLoadConstantDataField(
NameRef const& name, PropertyAccessInfo const& access_info,
Node* lookup_start_object) {
if (!access_info.IsFastDataConstant()) return nullptr;
......@@ -274,8 +292,8 @@ Node* PropertyAccessBuilder::BuildLoadDataField(
Node* lookup_start_object, Node** effect, Node** control) {
DCHECK(access_info.IsDataField() || access_info.IsFastDataConstant());
if (Node* value = TryBuildLoadConstantDataField(name, access_info,
lookup_start_object)) {
if (Node* value = TryFoldLoadConstantDataField(name, access_info,
lookup_start_object)) {
return value;
}
......
......@@ -64,6 +64,10 @@ class PropertyAccessBuilder {
Node* lookup_start_object, Node** effect,
Node** control);
// Loads a constant value from a prototype object in dictionary mode and
// constant-folds it.
Node* FoldLoadDictPrototypeConstant(PropertyAccessInfo const& access_info);
// Builds the load for data-field access for minimorphic loads that use
// dynamic map checks. These cannot depend on any information from the maps.
Node* BuildMinimorphicLoadDataField(
......@@ -82,9 +86,9 @@ class PropertyAccessBuilder {
CommonOperatorBuilder* common() const;
SimplifiedOperatorBuilder* simplified() const;
Node* TryBuildLoadConstantDataField(NameRef const& name,
PropertyAccessInfo const& access_info,
Node* lookup_start_object);
Node* TryFoldLoadConstantDataField(NameRef const& name,
PropertyAccessInfo const& access_info,
Node* lookup_start_object);
// Returns a node with the holder for the property access described by
// {access_info}.
Node* ResolveHolder(PropertyAccessInfo const& access_info,
......
......@@ -3059,8 +3059,10 @@ SerializerForBackgroundCompilation::ProcessMapForNamedPropertyAccess(
switch (access_mode) {
case AccessMode::kLoad:
// For PropertyAccessBuilder::TryBuildLoadConstantDataField
if (access_info.IsFastDataConstant()) {
// For PropertyAccessBuilder::TryBuildLoadConstantDataField and
// PropertyAccessBuilder::BuildLoadDictPrototypeConstant
if (access_info.IsFastDataConstant() ||
access_info.IsDictionaryProtoDataConstant()) {
base::Optional<JSObjectRef> holder;
Handle<JSObject> prototype;
if (access_info.holder().ToHandle(&prototype)) {
......@@ -3072,9 +3074,14 @@ SerializerForBackgroundCompilation::ProcessMapForNamedPropertyAccess(
}
if (holder.has_value()) {
base::Optional<ObjectRef> constant(holder->GetOwnFastDataProperty(
access_info.field_representation(), access_info.field_index(),
SerializationPolicy::kSerializeIfNeeded));
SerializationPolicy policy = SerializationPolicy::kSerializeIfNeeded;
base::Optional<ObjectRef> constant =
access_info.IsFastDataConstant()
? holder->GetOwnFastDataProperty(
access_info.field_representation(),
access_info.field_index(), policy)
: holder->GetOwnDictionaryProperty(
access_info.dictionary_index(), policy);
if (constant.has_value()) {
result_hints->AddConstant(constant->object(), zone(), broker());
}
......
......@@ -660,9 +660,14 @@ class DependentCode : public WeakFixedArray {
// deoptimized when the transition is replaced by a new version.
kTransitionGroup,
// Group of code that omit run-time prototype checks for prototypes
// described by this map. The group is deoptimized whenever an object
// described by this map changes shape (and transitions to a new map),
// possibly invalidating the assumptions embedded in the code.
// described by this map. The group is deoptimized whenever the following
// conditions hold, possibly invalidating the assumptions embedded in the
// code:
// a) A fast-mode object described by this map changes shape (and
// transitions to a new map), or
// b) A dictionary-mode prototype described by this map changes shape, the
// const-ness of one of its properties changes, or its [[Prototype]]
// changes (only the latter causes a transition).
kPrototypeCheckGroup,
// Group of code that depends on global property values in property cells
// not being changed.
......
......@@ -4160,6 +4160,20 @@ Handle<Object> JSObject::FastPropertyAt(Handle<JSObject> object,
return Object::WrapForRead(isolate, raw_value, representation);
}
// static
Handle<Object> JSObject::DictionaryPropertyAt(Handle<JSObject> object,
InternalIndex dict_index) {
Isolate* isolate = object->GetIsolate();
if (V8_DICT_MODE_PROTOTYPES_BOOL) {
SwissNameDictionary dict = object->property_dictionary_swiss();
return handle(dict.ValueAt(dict_index), isolate);
} else {
NameDictionary dict = object->property_dictionary();
return handle(dict.ValueAt(dict_index), isolate);
}
}
// TODO(cbruni/jkummerow): Consider moving this into elements.cc.
bool JSObject::HasEnumerableElements() {
// TODO(cbruni): cleanup
......@@ -4577,6 +4591,22 @@ void InvalidateOnePrototypeValidityCellInternal(Map map) {
PrototypeInfo prototype_info = PrototypeInfo::cast(maybe_prototype_info);
prototype_info.set_prototype_chain_enum_cache(Object());
}
// We may inline accesses to constants stored in dictionary mode protoypes in
// optimized code. When doing so, we install depenendies of group
// |kPrototypeCheckGroup| on each prototype between the receiver's immediate
// prototype and the holder of the constant property. This dependency is used
// both to detect changes to the constant value itself, and other changes to
// the prototype chain that invalidate the access to the given property from
// the given receiver (like adding the property to another prototype between
// the receiver and the (previous) holder). This works by de-opting this group
// whenever the validity cell would be invalidated. However, the actual value
// of the validity cell is not used. Therefore, we always trigger the de-opt
// here, even if the cell was already invalid.
if (V8_DICT_PROPERTY_CONST_TRACKING_BOOL && map.is_dictionary_map()) {
map.dependent_code().DeoptimizeDependentCodeGroup(
DependentCode::kPrototypeCheckGroup);
}
}
void InvalidatePrototypeChainsInternal(Map map) {
......
......@@ -643,6 +643,10 @@ class JSObject : public TorqueGeneratedJSObject<JSObject, JSReceiver> {
int unused_property_fields,
const char* reason);
// Access property in dictionary mode object at the given dictionary index.
static Handle<Object> DictionaryPropertyAt(Handle<JSObject> object,
InternalIndex dict_index);
// Access fast-case object properties at index.
static Handle<Object> FastPropertyAt(Handle<JSObject> object,
Representation representation,
......
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Flags: --allow-natives-syntax
// Flags: --allow-natives-syntax --opt --no-always-opt
//
// Tests tracking of constness of properties stored in dictionary
// mode prototypes.
......@@ -260,3 +260,403 @@ function testbench(o, proto, update_proto, check_constness) {
testbench(o, proto, update_z, false);
})();
//
// Below: Testing TF optimization of accessing constants in dictionary mode
// protoypes.
//
// Test inlining with fast mode receiver.
(function() {
var proto = Object.create(null);
proto.x = 1;
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
// Test that we inlined the access:
var dummy = {x : 123};
read_x(dummy);
if (%IsDictPropertyConstTrackingEnabled()) {
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
assertUnoptimized(read_x);
}
})();
// Test inlining with dictionary mode receiver that is a prototype.
(function() {
var proto1 = Object.create(null);
proto1.x = 1;
var proto2 = Object.create(null);
var o = Object.create(proto1);
Object.setPrototypeOf(proto1, proto2);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto1));
assertFalse(%HasFastProperties(proto2));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(proto1));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(proto1));
assertOptimized(read_x);
// Test that we inlined the access:
var dummy = {x : 123};
read_x(dummy);
// TODO(v8:11457) This test doesn't work yet, see TODO in
// AccessInfoFactory::TryLoadPropertyDetails. Currently, we can't inline
// accesses with dictionary mode receivers.
// if (%IsDictPropertyConstTrackingEnabled()) {
// assertTrue(%HasFastProperties(o));
// assertFalse(%HasFastProperties(proto1));
// assertFalse(%HasFastProperties(proto2));
// assertUnoptimized(read_x);
// }
})();
// The machinery we use for detecting the invalidation of constants held by
// dictionary mode objects (related to the prototype validity cell mechanism) is
// specific to prototypes. This means that for non-prototype dictionary mode
// objects, we have no way of detecting changes invalidating folded
// constants. Therefore, we must not fold constants held by non-prototype
// dictionary mode objects. This is tested here.
(function() {
var proto = Object.create(null);
proto.x = 1;
var o = Object.create(null);
Object.setPrototypeOf(o, proto);
assertFalse(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
var dummy = {x : 123};
read_x(dummy);
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
// We never inlined the acceess, so it's still optimized.
assertOptimized(read_x);
}
})();
// Invalidation by adding same property to receiver.
(function() {
var proto = Object.create(null);
proto.x = 1;
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
o.x = 2;
assertEquals(2, read_x(o));
if (%IsDictPropertyConstTrackingEnabled()) {
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
assertUnoptimized(read_x);
}
})();
// Invalidation by adding property to intermediate prototype.
(function() {
var proto = Object.create(null);
proto.x = 1;
var in_between = Object.create(null);
Object.setPrototypeOf(in_between, proto);
var o = Object.create(in_between);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(in_between));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
in_between.x = 2;
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(in_between));
assertFalse(%HasFastProperties(proto));
assertUnoptimized(read_x);
}
assertEquals(2, read_x(o));
})();
// Invalidation by changing prototype of receiver.
(function() {
var proto = Object.create(null);
proto.x = 1;
var other_proto = Object.create(null);
other_proto.x = 2;
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
Object.setPrototypeOf(o, other_proto);
assertEquals(2, read_x(o));
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertFalse(%HasFastProperties(other_proto));
assertUnoptimized(read_x);
}
})();
// Invalidation by changing [[Prototype]] of a prototype on the chain from the
// receiver to the holder.
(function() {
var proto = Object.create(null);
proto.x = 1;
var other_proto = Object.create(null);
other_proto.x = 2;
var in_between = Object.create(null);
Object.setPrototypeOf(in_between, proto);
var o = Object.create(in_between);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(in_between));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
Object.setPrototypeOf(in_between, other_proto);
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(in_between));
assertFalse(%HasFastProperties(proto));
assertFalse(%HasFastProperties(other_proto));
assertUnoptimized(read_x);
}
assertEquals(2, read_x(o));
})();
// Invalidation by changing property on prototype itself.
(function() {
var proto = Object.create(null);
proto.x = 1;
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
assertEquals(1, read_x(o));
%OptimizeFunctionOnNextCall(read_x);
assertEquals(1, read_x(o));
assertOptimized(read_x);
proto.x = 2;
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertUnoptimized(read_x);
}
assertEquals(2, read_x(o));
})();
// Invalidation by deleting property on prototype.
(function() {
var proto = Object.create(null);
proto.x = 1;
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_x(arg_o) {
return arg_o.x;
}
%PrepareFunctionForOptimization(read_x);
read_x(o);
%OptimizeFunctionOnNextCall(read_x);
read_x(o);
delete proto.x;
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertUnoptimized(read_x);
}
assertEquals(undefined, read_x(o));
})();
// Storing the same value does not invalidate const-ness. Store done from
// runtime/without feedback.
(function() {
var proto = Object.create(null);
var some_object = {bla: 123};
proto.x = 1;
proto.y = some_object
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_xy(arg_o) {
return [arg_o.x, arg_o.y];
}
%PrepareFunctionForOptimization(read_xy);
assertEquals([1, some_object], read_xy(o));
%OptimizeFunctionOnNextCall(read_xy);
assertEquals([1, some_object], read_xy(o));
assertOptimized(read_xy);
// Build value 1 without re-using proto.x.
var x2 = 0;
for(var i = 0; i < 5; ++i) {
x2 += 0.2;
}
// Storing the same values for x and y again:
proto.x = x2;
proto.y = some_object;
assertEquals(x2, proto.x);
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertTrue(%HasOwnConstDataProperty(proto, "x"));
assertOptimized(read_xy);
}
proto.x = 2;
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertFalse(%HasOwnConstDataProperty(proto, "x"));
assertUnoptimized(read_xy);
}
assertEquals(2, read_xy(o)[0]);
})();
// Storing the same value does not invalidate const-ness. Store done by IC
// handler.
(function() {
var proto = Object.create(null);
var some_object = {bla: 123};
proto.x = 1;
proto.y = some_object
var o = Object.create(proto);
assertTrue(%HasFastProperties(o));
assertFalse(%HasFastProperties(proto));
function read_xy(arg_o) {
return [arg_o.x, arg_o.y];
}
%PrepareFunctionForOptimization(read_xy);
assertEquals([1, some_object], read_xy(o));
%OptimizeFunctionOnNextCall(read_xy);
assertEquals([1, some_object], read_xy(o));
assertOptimized(read_xy);
// Build value 1 without re-using proto.x.
var x2 = 0;
for(var i = 0; i < 5; ++i) {
x2 += 0.2;
}
function change_xy(obj, x, y) {
obj.x = x;
obj.y = y;
}
%PrepareFunctionForOptimization(change_xy);
// Storing the same values for x and y again:
change_xy(proto, 1, some_object);
change_xy(proto, 1, some_object);
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertTrue(%HasOwnConstDataProperty(proto, "x"));
assertOptimized(read_xy);
}
change_xy(proto, 2, some_object);
if (%IsDictPropertyConstTrackingEnabled()) {
assertFalse(%HasFastProperties(proto));
assertFalse(%HasOwnConstDataProperty(proto, "x"));
assertUnoptimized(read_xy);
}
assertEquals(2, read_xy(o)[0]);
})();
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