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