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