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