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