PropertyMap.java revision 1416:a750a66640e0
1/*
2 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package jdk.nashorn.internal.runtime;
27
28import static jdk.nashorn.internal.runtime.PropertyHashMap.EMPTY_HASHMAP;
29import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.getArrayIndex;
30import static jdk.nashorn.internal.runtime.arrays.ArrayIndex.isValidArrayIndex;
31
32import java.io.IOException;
33import java.io.ObjectInputStream;
34import java.io.ObjectOutputStream;
35import java.io.Serializable;
36import java.lang.invoke.SwitchPoint;
37import java.lang.ref.SoftReference;
38import java.util.Arrays;
39import java.util.BitSet;
40import java.util.Collection;
41import java.util.HashMap;
42import java.util.Iterator;
43import java.util.NoSuchElementException;
44import java.util.WeakHashMap;
45import java.util.concurrent.atomic.LongAdder;
46import jdk.nashorn.internal.scripts.JO;
47
48/**
49 * Map of object properties. The PropertyMap is the "template" for JavaScript object
50 * layouts. It contains a map with prototype names as keys and {@link Property} instances
51 * as values. A PropertyMap is typically passed to the {@link ScriptObject} constructor
52 * to form the seed map for the ScriptObject.
53 * <p>
54 * All property maps are immutable. If a property is added, modified or removed, the mutator
55 * will return a new map.
56 */
57public final class PropertyMap implements Iterable<Object>, Serializable {
58    /** Used for non extensible PropertyMaps, negative logic as the normal case is extensible. See {@link ScriptObject#preventExtensions()} */
59    public static final int NOT_EXTENSIBLE        = 0b0000_0001;
60    /** Does this map contain valid array keys? */
61    public static final int CONTAINS_ARRAY_KEYS   = 0b0000_0010;
62
63    /** Map status flags. */
64    private int flags;
65
66    /** Map of properties. */
67    private transient PropertyHashMap properties;
68
69    /** Number of fields in use. */
70    private int fieldCount;
71
72    /** Number of fields available. */
73    private final int fieldMaximum;
74
75    /** Length of spill in use. */
76    private int spillLength;
77
78    /** Structure class name */
79    private String className;
80
81    /** {@link SwitchPoint}s for gets on inherited properties. */
82    private transient HashMap<String, SwitchPoint> protoGetSwitches;
83
84    /** History of maps, used to limit map duplication. */
85    private transient WeakHashMap<Property, SoftReference<PropertyMap>> history;
86
87    /** History of prototypes, used to limit map duplication. */
88    private transient WeakHashMap<ScriptObject, SoftReference<PropertyMap>> protoHistory;
89
90    /** property listeners */
91    private transient PropertyListeners listeners;
92
93    private transient BitSet freeSlots;
94
95    private static final long serialVersionUID = -7041836752008732533L;
96
97    /**
98     * Constructor.
99     *
100     * @param properties   A {@link PropertyHashMap} with initial contents.
101     * @param fieldCount   Number of fields in use.
102     * @param fieldMaximum Number of fields available.
103     * @param spillLength  Number of spill slots used.
104     * @param containsArrayKeys True if properties contain numeric keys
105     */
106    private PropertyMap(final PropertyHashMap properties, final String className, final int fieldCount,
107                        final int fieldMaximum, final int spillLength, final boolean containsArrayKeys) {
108        this.properties   = properties;
109        this.className    = className;
110        this.fieldCount   = fieldCount;
111        this.fieldMaximum = fieldMaximum;
112        this.spillLength  = spillLength;
113        if (containsArrayKeys) {
114            setContainsArrayKeys();
115        }
116
117        if (Context.DEBUG) {
118            count.increment();
119        }
120    }
121
122    /**
123     * Cloning constructor.
124     *
125     * @param propertyMap Existing property map.
126     * @param properties  A {@link PropertyHashMap} with a new set of properties.
127     */
128    private PropertyMap(final PropertyMap propertyMap, final PropertyHashMap properties) {
129        this.properties   = properties;
130        this.flags        = propertyMap.flags;
131        this.spillLength  = propertyMap.spillLength;
132        this.fieldCount   = propertyMap.fieldCount;
133        this.fieldMaximum = propertyMap.fieldMaximum;
134        // We inherit the parent property listeners instance. It will be cloned when a new listener is added.
135        this.listeners    = propertyMap.listeners;
136        this.freeSlots    = propertyMap.freeSlots;
137
138        if (Context.DEBUG) {
139            count.increment();
140            clonedCount.increment();
141        }
142    }
143
144    /**
145     * Cloning constructor.
146     *
147     * @param propertyMap Existing property map.
148      */
149    private PropertyMap(final PropertyMap propertyMap) {
150        this(propertyMap, propertyMap.properties);
151    }
152
153    private void writeObject(final ObjectOutputStream out) throws IOException {
154        out.defaultWriteObject();
155        out.writeObject(properties.getProperties());
156    }
157
158    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
159        in.defaultReadObject();
160
161        final Property[] props = (Property[]) in.readObject();
162        this.properties = EMPTY_HASHMAP.immutableAdd(props);
163
164        assert className != null;
165        final Class<?> structure = Context.forStructureClass(className);
166        for (final Property prop : props) {
167            prop.initMethodHandles(structure);
168        }
169    }
170
171    /**
172     * Public property map allocator.
173     *
174     * <p>It is the caller's responsibility to make sure that {@code properties} does not contain
175     * properties with keys that are valid array indices.</p>
176     *
177     * @param properties   Collection of initial properties.
178     * @param className    class name
179     * @param fieldCount   Number of fields in use.
180     * @param fieldMaximum Number of fields available.
181     * @param spillLength  Number of used spill slots.
182     * @return New {@link PropertyMap}.
183     */
184    public static PropertyMap newMap(final Collection<Property> properties, final String className, final int fieldCount, final int fieldMaximum,  final int spillLength) {
185        final PropertyHashMap newProperties = EMPTY_HASHMAP.immutableAdd(properties);
186        return new PropertyMap(newProperties, className, fieldCount, fieldMaximum, spillLength, false);
187    }
188
189    /**
190     * Public property map allocator. Used by nasgen generated code.
191     *
192     * <p>It is the caller's responsibility to make sure that {@code properties} does not contain
193     * properties with keys that are valid array indices.</p>
194     *
195     * @param properties Collection of initial properties.
196     * @return New {@link PropertyMap}.
197     */
198    public static PropertyMap newMap(final Collection<Property> properties) {
199        return properties == null || properties.isEmpty()? newMap() : newMap(properties, JO.class.getName(), 0, 0, 0);
200    }
201
202    /**
203     * Return a sharable empty map for the given object class.
204     * @param clazz the base object class
205     * @return New empty {@link PropertyMap}.
206     */
207    public static PropertyMap newMap(final Class<? extends ScriptObject> clazz) {
208        return new PropertyMap(EMPTY_HASHMAP, clazz.getName(), 0, 0, 0, false);
209    }
210
211    /**
212     * Return a sharable empty map.
213     *
214     * @return New empty {@link PropertyMap}.
215     */
216    public static PropertyMap newMap() {
217        return newMap(JO.class);
218    }
219
220    /**
221     * Return number of properties in the map.
222     *
223     * @return Number of properties.
224     */
225    public int size() {
226        return properties.size();
227    }
228
229    /**
230     * Get the listeners of this map, or null if none exists
231     *
232     * @return the listeners
233     */
234    public PropertyListeners getListeners() {
235        return listeners;
236    }
237
238    /**
239     * Add {@code listenerMap} as a listener to this property map for the given {@code key}.
240     *
241     * @param key the property name
242     * @param listenerMap the listener map
243     */
244    public void addListener(final String key, final PropertyMap listenerMap) {
245        if (listenerMap != this) {
246            // We need to clone listener instance when adding a new listener since we share
247            // the listeners instance with our parent maps that don't need to see the new listener.
248            listeners = PropertyListeners.addListener(listeners, key, listenerMap);
249        }
250    }
251
252    /**
253     * A new property is being added.
254     *
255     * @param property The new Property added.
256     */
257    public void propertyAdded(final Property property) {
258        invalidateProtoGetSwitchPoint(property);
259        if (listeners != null) {
260            listeners.propertyAdded(property);
261        }
262    }
263
264    /**
265     * An existing property is being deleted.
266     *
267     * @param property The property being deleted.
268     */
269    public void propertyDeleted(final Property property) {
270        invalidateProtoGetSwitchPoint(property);
271        if (listeners != null) {
272            listeners.propertyDeleted(property);
273        }
274    }
275
276    /**
277     * An existing property is being redefined.
278     *
279     * @param oldProperty The old property
280     * @param newProperty The new property
281     */
282    public void propertyModified(final Property oldProperty, final Property newProperty) {
283        invalidateProtoGetSwitchPoint(oldProperty);
284        if (listeners != null) {
285            listeners.propertyModified(oldProperty, newProperty);
286        }
287    }
288
289    /**
290     * The prototype of an object associated with this {@link PropertyMap} is changed.
291     */
292    public void protoChanged() {
293        invalidateAllProtoGetSwitchPoints();
294        if (listeners != null) {
295            listeners.protoChanged();
296        }
297    }
298
299    /**
300     * Return a SwitchPoint used to track changes of a property in a prototype.
301     *
302     * @param key Property key.
303     * @return A shared {@link SwitchPoint} for the property.
304     */
305    public synchronized SwitchPoint getSwitchPoint(final String key) {
306        if (protoGetSwitches == null) {
307            protoGetSwitches = new HashMap<>();
308        }
309
310        SwitchPoint switchPoint = protoGetSwitches.get(key);
311        if (switchPoint == null) {
312            switchPoint = new SwitchPoint();
313            protoGetSwitches.put(key, switchPoint);
314        }
315
316        return switchPoint;
317    }
318
319    /**
320     * Indicate that a prototype property has changed.
321     *
322     * @param property {@link Property} to invalidate.
323     */
324    synchronized void invalidateProtoGetSwitchPoint(final Property property) {
325        if (protoGetSwitches != null) {
326
327            final String key = property.getKey();
328            final SwitchPoint sp = protoGetSwitches.get(key);
329            if (sp != null) {
330                protoGetSwitches.remove(key);
331                if (Context.DEBUG) {
332                    protoInvalidations.increment();
333                }
334                SwitchPoint.invalidateAll(new SwitchPoint[] { sp });
335            }
336        }
337    }
338
339    /**
340     * Indicate that proto itself has changed in hierarchy somewhere.
341     */
342    synchronized void invalidateAllProtoGetSwitchPoints() {
343        if (protoGetSwitches != null) {
344            final int size = protoGetSwitches.size();
345            if (size > 0) {
346                if (Context.DEBUG) {
347                    protoInvalidations.add(size);
348                }
349                SwitchPoint.invalidateAll(protoGetSwitches.values().toArray(new SwitchPoint[size]));
350                protoGetSwitches.clear();
351            }
352        }
353    }
354
355    /**
356     * Add a property to the map, re-binding its getters and setters,
357     * if available, to a given receiver. This is typically the global scope. See
358     * {@link ScriptObject#addBoundProperties(ScriptObject)}
359     *
360     * @param property {@link Property} being added.
361     * @param bindTo   Object to bind to.
362     *
363     * @return New {@link PropertyMap} with {@link Property} added.
364     */
365    PropertyMap addPropertyBind(final AccessorProperty property, final Object bindTo) {
366        // No need to store bound property in the history as bound properties can't be reused.
367        return addPropertyNoHistory(new AccessorProperty(property, bindTo));
368    }
369
370    // Get a logical slot index for a property, with spill slot 0 starting at fieldMaximum.
371    private int logicalSlotIndex(final Property property) {
372        final int slot = property.getSlot();
373        if (slot < 0) {
374            return -1;
375        }
376        return property.isSpill() ? slot + fieldMaximum : slot;
377    }
378
379    // Update boundaries and flags after a property has been added
380    private void updateFlagsAndBoundaries(final Property newProperty) {
381        if(newProperty.isSpill()) {
382            spillLength = Math.max(spillLength, newProperty.getSlot() + 1);
383        } else {
384            fieldCount = Math.max(fieldCount, newProperty.getSlot() + 1);
385        }
386        if (isValidArrayIndex(getArrayIndex(newProperty.getKey()))) {
387            setContainsArrayKeys();
388        }
389    }
390
391    // Update the free slots bitmap for a property that has been deleted and/or added. This method is not synchronized
392    // as it is always invoked on a newly created instance.
393    private void updateFreeSlots(final Property oldProperty, final Property newProperty) {
394        // Free slots bitset is possibly shared with parent map, so we must clone it before making modifications.
395        boolean freeSlotsCloned = false;
396        if (oldProperty != null) {
397            final int slotIndex = logicalSlotIndex(oldProperty);
398            if (slotIndex >= 0) {
399                final BitSet newFreeSlots = freeSlots == null ? new BitSet() : (BitSet)freeSlots.clone();
400                assert !newFreeSlots.get(slotIndex);
401                newFreeSlots.set(slotIndex);
402                freeSlots = newFreeSlots;
403                freeSlotsCloned = true;
404            }
405        }
406        if (freeSlots != null && newProperty != null) {
407            final int slotIndex = logicalSlotIndex(newProperty);
408            if (slotIndex > -1 && freeSlots.get(slotIndex)) {
409                final BitSet newFreeSlots = freeSlotsCloned ? freeSlots : ((BitSet)freeSlots.clone());
410                newFreeSlots.clear(slotIndex);
411                freeSlots = newFreeSlots.isEmpty() ? null : newFreeSlots;
412            }
413        }
414    }
415
416    /**
417     * Add a property to the map without adding it to the history. This should be used for properties that
418     * can't be shared such as bound properties, or properties that are expected to be added only once.
419     *
420     * @param property {@link Property} being added.
421     * @return New {@link PropertyMap} with {@link Property} added.
422     */
423    public PropertyMap addPropertyNoHistory(final Property property) {
424        if (listeners != null) {
425            listeners.propertyAdded(property);
426        }
427        final PropertyHashMap newProperties = properties.immutableAdd(property);
428        final PropertyMap newMap = new PropertyMap(this, newProperties);
429        newMap.updateFlagsAndBoundaries(property);
430        newMap.updateFreeSlots(null, property);
431
432        return newMap;
433    }
434
435    /**
436     * Add a property to the map.  Cloning or using an existing map if available.
437     *
438     * @param property {@link Property} being added.
439     *
440     * @return New {@link PropertyMap} with {@link Property} added.
441     */
442    public synchronized PropertyMap addProperty(final Property property) {
443        if (listeners != null) {
444            listeners.propertyAdded(property);
445        }
446        PropertyMap newMap = checkHistory(property);
447
448        if (newMap == null) {
449            final PropertyHashMap newProperties = properties.immutableAdd(property);
450            newMap = new PropertyMap(this, newProperties);
451            newMap.updateFlagsAndBoundaries(property);
452            newMap.updateFreeSlots(null, property);
453            addToHistory(property, newMap);
454        }
455
456        return newMap;
457    }
458
459    /**
460     * Remove a property from a map. Cloning or using an existing map if available.
461     *
462     * @param property {@link Property} being removed.
463     *
464     * @return New {@link PropertyMap} with {@link Property} removed or {@code null} if not found.
465     */
466    public synchronized PropertyMap deleteProperty(final Property property) {
467        if (listeners != null) {
468            listeners.propertyDeleted(property);
469        }
470        PropertyMap newMap = checkHistory(property);
471        final String key = property.getKey();
472
473        if (newMap == null && properties.containsKey(key)) {
474            final PropertyHashMap newProperties = properties.immutableRemove(key);
475            final boolean isSpill = property.isSpill();
476            final int slot = property.getSlot();
477            // If deleted property was last field or spill slot we can make it reusable by reducing field/slot count.
478            // Otherwise mark it as free in free slots bitset.
479            if (isSpill && slot >= 0 && slot == spillLength - 1) {
480                newMap = new PropertyMap(newProperties, className, fieldCount, fieldMaximum, spillLength - 1, containsArrayKeys());
481                newMap.freeSlots = freeSlots;
482            } else if (!isSpill && slot >= 0 && slot == fieldCount - 1) {
483                newMap = new PropertyMap(newProperties, className, fieldCount - 1, fieldMaximum, spillLength, containsArrayKeys());
484                newMap.freeSlots = freeSlots;
485            } else {
486                newMap = new PropertyMap(this, newProperties);
487                newMap.updateFreeSlots(property, null);
488            }
489            addToHistory(property, newMap);
490        }
491
492        return newMap;
493    }
494
495    /**
496     * Replace an existing property with a new one.
497     *
498     * @param oldProperty Property to replace.
499     * @param newProperty New {@link Property}.
500     *
501     * @return New {@link PropertyMap} with {@link Property} replaced.
502     */
503    public PropertyMap replaceProperty(final Property oldProperty, final Property newProperty) {
504        if (listeners != null) {
505            listeners.propertyModified(oldProperty, newProperty);
506        }
507        // Add replaces existing property.
508        final PropertyHashMap newProperties = properties.immutableReplace(oldProperty, newProperty);
509        final PropertyMap newMap = new PropertyMap(this, newProperties);
510        /*
511         * See ScriptObject.modifyProperty and ScriptObject.setUserAccessors methods.
512         *
513         * This replaceProperty method is called only for the following three cases:
514         *
515         *   1. To change flags OR TYPE of an old (cloned) property. We use the same spill slots.
516         *   2. To change one UserAccessor property with another - user getter or setter changed via
517         *      Object.defineProperty function. Again, same spill slots are re-used.
518         *   3. Via ScriptObject.setUserAccessors method to set user getter and setter functions
519         *      replacing the dummy AccessorProperty with null method handles (added during map init).
520         *
521         * In case (1) and case(2), the property type of old and new property is same. For case (3),
522         * the old property is an AccessorProperty and the new one is a UserAccessorProperty property.
523         */
524
525        final boolean sameType = oldProperty.getClass() == newProperty.getClass();
526        assert sameType ||
527                oldProperty instanceof AccessorProperty &&
528                newProperty instanceof UserAccessorProperty :
529            "arbitrary replaceProperty attempted " + sameType + " oldProperty=" + oldProperty.getClass() + " newProperty=" + newProperty.getClass() + " [" + oldProperty.getLocalType() + " => " + newProperty.getLocalType() + "]";
530
531        newMap.flags = flags;
532
533        /*
534         * spillLength remains same in case (1) and (2) because of slot reuse. Only for case (3), we need
535         * to add spill count of the newly added UserAccessorProperty property.
536         */
537        if (!sameType) {
538            newMap.spillLength = Math.max(spillLength, newProperty.getSlot() + 1);
539            newMap.updateFreeSlots(oldProperty, newProperty);
540        }
541        return newMap;
542    }
543
544    /**
545     * Make a new UserAccessorProperty property. getter and setter functions are stored in
546     * this ScriptObject and slot values are used in property object. Note that slots
547     * are assigned speculatively and should be added to map before adding other
548     * properties.
549     *
550     * @param key the property name
551     * @param propertyFlags attribute flags of the property
552     * @return the newly created UserAccessorProperty
553     */
554    public UserAccessorProperty newUserAccessors(final String key, final int propertyFlags) {
555        return new UserAccessorProperty(key, propertyFlags, getFreeSpillSlot());
556    }
557
558    /**
559     * Find a property in the map.
560     *
561     * @param key Key to search for.
562     *
563     * @return {@link Property} matching key.
564     */
565    public Property findProperty(final String key) {
566        return properties.find(key);
567    }
568
569    /**
570     * Adds all map properties from another map.
571     *
572     * @param other The source of properties.
573     *
574     * @return New {@link PropertyMap} with added properties.
575     */
576    public PropertyMap addAll(final PropertyMap other) {
577        assert this != other : "adding property map to itself";
578        final Property[] otherProperties = other.properties.getProperties();
579        final PropertyHashMap newProperties = properties.immutableAdd(otherProperties);
580
581        final PropertyMap newMap = new PropertyMap(this, newProperties);
582        for (final Property property : otherProperties) {
583            // This method is only safe to use with non-slotted, native getter/setter properties
584            assert property.getSlot() == -1;
585            assert !(isValidArrayIndex(getArrayIndex(property.getKey())));
586        }
587
588        return newMap;
589    }
590
591    /**
592     * Return an array of all properties.
593     *
594     * @return Properties as an array.
595     */
596    public Property[] getProperties() {
597        return properties.getProperties();
598    }
599
600    /**
601     * Return the name of the class of objects using this property map.
602     *
603     * @return class name of owner objects.
604     */
605    public String getClassName() {
606        return className;
607    }
608
609    /**
610     * Prevents the map from having additional properties.
611     *
612     * @return New map with {@link #NOT_EXTENSIBLE} flag set.
613     */
614    PropertyMap preventExtensions() {
615        final PropertyMap newMap = new PropertyMap(this);
616        newMap.flags |= NOT_EXTENSIBLE;
617        return newMap;
618    }
619
620    /**
621     * Prevents properties in map from being modified.
622     *
623     * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with
624     * {@link Property#NOT_CONFIGURABLE} set.
625     */
626    PropertyMap seal() {
627        PropertyHashMap newProperties = EMPTY_HASHMAP;
628
629        for (final Property oldProperty :  properties.getProperties()) {
630            newProperties = newProperties.immutableAdd(oldProperty.addFlags(Property.NOT_CONFIGURABLE));
631        }
632
633        final PropertyMap newMap = new PropertyMap(this, newProperties);
634        newMap.flags |= NOT_EXTENSIBLE;
635
636        return newMap;
637    }
638
639    /**
640     * Prevents properties in map from being modified or written to.
641     *
642     * @return New map with {@link #NOT_EXTENSIBLE} flag set and properties with
643     * {@link Property#NOT_CONFIGURABLE} and {@link Property#NOT_WRITABLE} set.
644     */
645    PropertyMap freeze() {
646        PropertyHashMap newProperties = EMPTY_HASHMAP;
647
648        for (final Property oldProperty : properties.getProperties()) {
649            int propertyFlags = Property.NOT_CONFIGURABLE;
650
651            if (!(oldProperty instanceof UserAccessorProperty)) {
652                propertyFlags |= Property.NOT_WRITABLE;
653            }
654
655            newProperties = newProperties.immutableAdd(oldProperty.addFlags(propertyFlags));
656        }
657
658        final PropertyMap newMap = new PropertyMap(this, newProperties);
659        newMap.flags |= NOT_EXTENSIBLE;
660
661        return newMap;
662    }
663
664    /**
665     * Check for any configurable properties.
666     *
667     * @return {@code true} if any configurable.
668     */
669    private boolean anyConfigurable() {
670        for (final Property property : properties.getProperties()) {
671            if (property.isConfigurable()) {
672               return true;
673            }
674        }
675
676        return false;
677    }
678
679    /**
680     * Check if all properties are frozen.
681     *
682     * @return {@code true} if all are frozen.
683     */
684    private boolean allFrozen() {
685        for (final Property property : properties.getProperties()) {
686            // check if it is a data descriptor
687            if (!(property instanceof UserAccessorProperty)) {
688                if (property.isWritable()) {
689                    return false;
690                }
691            }
692            if (property.isConfigurable()) {
693               return false;
694            }
695        }
696
697        return true;
698    }
699
700    /**
701     * Check prototype history for an existing property map with specified prototype.
702     *
703     * @param proto New prototype object.
704     *
705     * @return Existing {@link PropertyMap} or {@code null} if not found.
706     */
707    private PropertyMap checkProtoHistory(final ScriptObject proto) {
708        final PropertyMap cachedMap;
709        if (protoHistory != null) {
710            final SoftReference<PropertyMap> weakMap = protoHistory.get(proto);
711            cachedMap = (weakMap != null ? weakMap.get() : null);
712        } else {
713            cachedMap = null;
714        }
715
716        if (Context.DEBUG && cachedMap != null) {
717            protoHistoryHit.increment();
718        }
719
720        return cachedMap;
721    }
722
723    /**
724     * Add a map to the prototype history.
725     *
726     * @param newProto Prototype to add (key.)
727     * @param newMap   {@link PropertyMap} associated with prototype.
728     */
729    private void addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap) {
730        if (protoHistory == null) {
731            protoHistory = new WeakHashMap<>();
732        }
733
734        protoHistory.put(newProto, new SoftReference<>(newMap));
735    }
736
737    /**
738     * Track the modification of the map.
739     *
740     * @param property Mapping property.
741     * @param newMap   Modified {@link PropertyMap}.
742     */
743    private void addToHistory(final Property property, final PropertyMap newMap) {
744        if (history == null) {
745            history = new WeakHashMap<>();
746        }
747
748        history.put(property, new SoftReference<>(newMap));
749    }
750
751    /**
752     * Check the history for a map that already has the given property added.
753     *
754     * @param property {@link Property} to add.
755     *
756     * @return Existing map or {@code null} if not found.
757     */
758    private PropertyMap checkHistory(final Property property) {
759
760        if (history != null) {
761            final SoftReference<PropertyMap> ref = history.get(property);
762            final PropertyMap historicMap = ref == null ? null : ref.get();
763
764            if (historicMap != null) {
765                if (Context.DEBUG) {
766                    historyHit.increment();
767                }
768
769                return historicMap;
770            }
771        }
772
773        return null;
774    }
775
776    /**
777     * Returns true if the two maps have identical properties in the same order, but allows the properties to differ in
778     * their types. This method is mostly useful for tests.
779     * @param otherMap the other map
780     * @return true if this map has identical properties in the same order as the other map, allowing the properties to
781     * differ in type.
782     */
783    public boolean equalsWithoutType(final PropertyMap otherMap) {
784        if (properties.size() != otherMap.properties.size()) {
785            return false;
786        }
787
788        final Iterator<Property> iter      = properties.values().iterator();
789        final Iterator<Property> otherIter = otherMap.properties.values().iterator();
790
791        while (iter.hasNext() && otherIter.hasNext()) {
792            if (!iter.next().equalsWithoutType(otherIter.next())) {
793                return false;
794            }
795        }
796
797        return true;
798    }
799
800    @Override
801    public String toString() {
802        final StringBuilder sb = new StringBuilder();
803
804        sb.append(Debug.id(this));
805        sb.append(" = {\n");
806
807        for (final Property property : getProperties()) {
808            sb.append('\t');
809            sb.append(property);
810            sb.append('\n');
811        }
812
813        sb.append('}');
814
815        return sb.toString();
816    }
817
818    @Override
819    public Iterator<Object> iterator() {
820        return new PropertyMapIterator(this);
821    }
822
823    /**
824     * Check if this map contains properties with valid array keys
825     *
826     * @return {@code true} if this map contains properties with valid array keys
827     */
828    public final boolean containsArrayKeys() {
829        return (flags & CONTAINS_ARRAY_KEYS) != 0;
830    }
831
832    /**
833     * Flag this object as having array keys in defined properties
834     */
835    private void setContainsArrayKeys() {
836        flags |= CONTAINS_ARRAY_KEYS;
837    }
838
839    /**
840     * Test to see if {@link PropertyMap} is extensible.
841     *
842     * @return {@code true} if {@link PropertyMap} can be added to.
843     */
844    boolean isExtensible() {
845        return (flags & NOT_EXTENSIBLE) == 0;
846    }
847
848    /**
849     * Test to see if {@link PropertyMap} is not extensible or any properties
850     * can not be modified.
851     *
852     * @return {@code true} if {@link PropertyMap} is sealed.
853     */
854    boolean isSealed() {
855        return !isExtensible() && !anyConfigurable();
856    }
857
858    /**
859     * Test to see if {@link PropertyMap} is not extensible or all properties
860     * can not be modified.
861     *
862     * @return {@code true} if {@link PropertyMap} is frozen.
863     */
864    boolean isFrozen() {
865        return !isExtensible() && allFrozen();
866    }
867
868    /**
869     * Return a free field slot for this map, or {@code -1} if none is available.
870     *
871     * @return free field slot or -1
872     */
873    int getFreeFieldSlot() {
874        if (freeSlots != null) {
875            final int freeSlot = freeSlots.nextSetBit(0);
876            if (freeSlot > -1 && freeSlot < fieldMaximum) {
877                return freeSlot;
878            }
879        }
880        if (fieldCount < fieldMaximum) {
881            return fieldCount;
882        }
883        return -1;
884    }
885
886    /**
887     * Get a free spill slot for this map.
888     *
889     * @return free spill slot
890     */
891    int getFreeSpillSlot() {
892        if (freeSlots != null) {
893            final int freeSlot = freeSlots.nextSetBit(fieldMaximum);
894            if (freeSlot > -1) {
895                return freeSlot - fieldMaximum;
896            }
897        }
898        return spillLength;
899    }
900
901    /**
902     * Return a property map with the same layout that is associated with the new prototype object.
903     *
904     * @param newProto New prototype object to replace oldProto.
905     * @return New {@link PropertyMap} with prototype changed.
906     */
907    public synchronized PropertyMap changeProto(final ScriptObject newProto) {
908        final PropertyMap nextMap = checkProtoHistory(newProto);
909        if (nextMap != null) {
910            return nextMap;
911        }
912
913        if (Context.DEBUG) {
914            setProtoNewMapCount.increment();
915        }
916
917        final PropertyMap newMap = new PropertyMap(this);
918        addToProtoHistory(newProto, newMap);
919
920        return newMap;
921    }
922
923
924    /**
925     * {@link PropertyMap} iterator.
926     */
927    private static class PropertyMapIterator implements Iterator<Object> {
928        /** Property iterator. */
929        final Iterator<Property> iter;
930
931        /** Current Property. */
932        Property property;
933
934        /**
935         * Constructor.
936         *
937         * @param propertyMap {@link PropertyMap} to iterate over.
938         */
939        PropertyMapIterator(final PropertyMap propertyMap) {
940            iter = Arrays.asList(propertyMap.properties.getProperties()).iterator();
941            property = iter.hasNext() ? iter.next() : null;
942            skipNotEnumerable();
943        }
944
945        /**
946         * Ignore properties that are not enumerable.
947         */
948        private void skipNotEnumerable() {
949            while (property != null && !property.isEnumerable()) {
950                property = iter.hasNext() ? iter.next() : null;
951            }
952        }
953
954        @Override
955        public boolean hasNext() {
956            return property != null;
957        }
958
959        @Override
960        public Object next() {
961            if (property == null) {
962                throw new NoSuchElementException();
963            }
964
965            final Object key = property.getKey();
966            property = iter.next();
967            skipNotEnumerable();
968
969            return key;
970        }
971
972        @Override
973        public void remove() {
974            throw new UnsupportedOperationException("remove");
975        }
976    }
977
978    /*
979     * Debugging and statistics.
980     */
981
982    /**
983     * Debug helper function that returns the diff of two property maps, only
984     * displaying the information that is different and in which map it exists
985     * compared to the other map. Can be used to e.g. debug map guards and
986     * investigate why they fail, causing relink
987     *
988     * @param map0 the first property map
989     * @param map1 the second property map
990     *
991     * @return property map diff as string
992     */
993    public static String diff(final PropertyMap map0, final PropertyMap map1) {
994        final StringBuilder sb = new StringBuilder();
995
996        if (map0 != map1) {
997           sb.append(">>> START: Map diff");
998           boolean found = false;
999
1000           for (final Property p : map0.getProperties()) {
1001               final Property p2 = map1.findProperty(p.getKey());
1002               if (p2 == null) {
1003                   sb.append("FIRST ONLY : [").append(p).append("]");
1004                   found = true;
1005               } else if (p2 != p) {
1006                   sb.append("DIFFERENT  : [").append(p).append("] != [").append(p2).append("]");
1007                   found = true;
1008               }
1009           }
1010
1011           for (final Property p2 : map1.getProperties()) {
1012               final Property p1 = map0.findProperty(p2.getKey());
1013               if (p1 == null) {
1014                   sb.append("SECOND ONLY: [").append(p2).append("]");
1015                   found = true;
1016               }
1017           }
1018
1019           //assert found;
1020
1021           if (!found) {
1022                sb.append(map0).
1023                    append("!=").
1024                    append(map1);
1025           }
1026
1027           sb.append("<<< END: Map diff\n");
1028        }
1029
1030        return sb.toString();
1031    }
1032
1033    // counters updated only in debug mode
1034    private static LongAdder count;
1035    private static LongAdder clonedCount;
1036    private static LongAdder historyHit;
1037    private static LongAdder protoInvalidations;
1038    private static LongAdder protoHistoryHit;
1039    private static LongAdder setProtoNewMapCount;
1040    static {
1041        if (Context.DEBUG) {
1042            count = new LongAdder();
1043            clonedCount = new LongAdder();
1044            historyHit = new LongAdder();
1045            protoInvalidations = new LongAdder();
1046            protoHistoryHit = new LongAdder();
1047            setProtoNewMapCount = new LongAdder();
1048        }
1049    }
1050
1051    /**
1052     * @return Total number of maps.
1053     */
1054    public static long getCount() {
1055        return count.longValue();
1056    }
1057
1058    /**
1059     * @return The number of maps that were cloned.
1060     */
1061    public static long getClonedCount() {
1062        return clonedCount.longValue();
1063    }
1064
1065    /**
1066     * @return The number of times history was successfully used.
1067     */
1068    public static long getHistoryHit() {
1069        return historyHit.longValue();
1070    }
1071
1072    /**
1073     * @return The number of times prototype changes caused invalidation.
1074     */
1075    public static long getProtoInvalidations() {
1076        return protoInvalidations.longValue();
1077    }
1078
1079    /**
1080     * @return The number of times proto history was successfully used.
1081     */
1082    public static long getProtoHistoryHit() {
1083        return protoHistoryHit.longValue();
1084    }
1085
1086    /**
1087     * @return The number of times prototypes were modified.
1088     */
1089    public static long getSetProtoNewMapCount() {
1090        return setProtoNewMapCount.longValue();
1091    }
1092}
1093