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