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.Reference; 38 import java.lang.ref.SoftReference; 39 import java.lang.ref.WeakReference; 40 import java.util.Arrays; 41 import java.util.BitSet; 42 import java.util.Collection; 43 import java.util.HashMap; 44 import java.util.Iterator; 45 import java.util.NoSuchElementException; 46 import java.util.WeakHashMap; 47 import java.util.concurrent.atomic.LongAdder; 48 import jdk.nashorn.internal.runtime.options.Options; 49 import 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 */ 60 public 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<String, 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 */ PropertyMap(final PropertyHashMap properties, final int flags, final String className, final int fieldCount, final int fieldMaximum, final int spillLength)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 */ PropertyMap(final PropertyMap propertyMap, final PropertyHashMap properties, final int flags, final int fieldCount, final int spillLength, final int softReferenceDerivationLimit)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 */ PropertyMap(final PropertyMap propertyMap)167 protected PropertyMap(final PropertyMap propertyMap) { 168 this(propertyMap, propertyMap.properties, propertyMap.flags, propertyMap.fieldCount, propertyMap.spillLength, propertyMap.softReferenceDerivationLimit); 169 } 170 writeObject(final ObjectOutputStream out)171 private void writeObject(final ObjectOutputStream out) throws IOException { 172 out.defaultWriteObject(); 173 out.writeObject(properties.getProperties()); 174 } 175 readObject(final ObjectInputStream in)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 */ newMap(final Collection<Property> properties, final String className, final int fieldCount, final int fieldMaximum, final int spillLength)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 */ newMap(final Collection<Property> properties)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 */ newMap(final Class<? extends ScriptObject> clazz)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 */ newMap()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 */ size()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 */ getListenerCount()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 */ addListener(final String key, final PropertyMap listenerMap)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 */ propertyAdded(final Property property, final boolean isSelf)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 */ propertyDeleted(final Property property, final boolean isSelf)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 */ propertyModified(final Property oldProperty, final Property newProperty, final boolean isSelf)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 */ protoChanged(final boolean isSelf)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 */ getSwitchPoint(final String key)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 */ invalidateProtoSwitchPoint(final String key)357 synchronized void invalidateProtoSwitchPoint(final String 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 */ invalidateAllProtoSwitchPoints()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[size])); 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 */ addPropertyBind(final AccessorProperty property, final Object bindTo)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. logicalSlotIndex(final Property property)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 newSpillLength(final Property newProperty)410 private int newSpillLength(final Property newProperty) { 411 return newProperty.isSpill() ? Math.max(spillLength, newProperty.getSlot() + 1) : spillLength; 412 } 413 newFieldCount(final Property newProperty)414 private int newFieldCount(final Property newProperty) { 415 return !newProperty.isSpill() ? Math.max(fieldCount, newProperty.getSlot() + 1) : fieldCount; 416 } 417 newFlags(final Property newProperty)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. updateFreeSlots(final Property oldProperty, final Property newProperty)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 */ addPropertyNoHistory(final Property property)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 */ addProperty(final Property property)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 deriveMap(final PropertyHashMap newProperties, final int newFlags, final int newFieldCount, final int newSpillLength)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 addPropertyInternal(final Property property)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 */ deleteProperty(final Property property)496 public final synchronized PropertyMap deleteProperty(final Property property) { 497 propertyDeleted(property, true); 498 PropertyMap newMap = checkHistory(property); 499 final String 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 */ replaceProperty(final Property oldProperty, final Property newProperty)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 */ newUserAccessors(final String key, final int propertyFlags)580 public final UserAccessorProperty newUserAccessors(final String 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 */ findProperty(final String key)591 public final Property findProperty(final String 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 */ addAll(final PropertyMap other)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 */ getProperties()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 */ getClassName()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 */ preventExtensions()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 */ seal()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 */ freeze()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 */ anyConfigurable()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 */ allFrozen()702 private boolean allFrozen() { 703 for (final Property property : properties.getProperties()) { 704 // check if it is a data descriptor 705 if (!(property instanceof UserAccessorProperty)) { 706 if (property.isWritable()) { 707 return false; 708 } 709 } 710 if (property.isConfigurable()) { 711 return false; 712 } 713 } 714 715 return true; 716 } 717 718 /** 719 * Check prototype history for an existing property map with specified prototype. 720 * 721 * @param proto New prototype object. 722 * 723 * @return Existing {@link PropertyMap} or {@code null} if not found. 724 */ checkProtoHistory(final ScriptObject proto)725 private PropertyMap checkProtoHistory(final ScriptObject proto) { 726 final PropertyMap cachedMap; 727 if (protoHistory != null) { 728 final SoftReference<PropertyMap> weakMap = protoHistory.get(proto); 729 cachedMap = (weakMap != null ? weakMap.get() : null); 730 } else { 731 cachedMap = null; 732 } 733 734 if (Context.DEBUG && cachedMap != null) { 735 protoHistoryHit.increment(); 736 } 737 738 return cachedMap; 739 } 740 741 /** 742 * Add a map to the prototype history. 743 * 744 * @param newProto Prototype to add (key.) 745 * @param newMap {@link PropertyMap} associated with prototype. 746 */ addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap)747 private void addToProtoHistory(final ScriptObject newProto, final PropertyMap newMap) { 748 if (protoHistory == null) { 749 protoHistory = new WeakHashMap<>(); 750 } 751 752 protoHistory.put(newProto, new SoftReference<>(newMap)); 753 } 754 755 /** 756 * Track the modification of the map. 757 * 758 * @param property Mapping property. 759 * @param newMap Modified {@link PropertyMap}. 760 */ addToHistory(final Property property, final PropertyMap newMap)761 private void addToHistory(final Property property, final PropertyMap newMap) { 762 if (history == null) { 763 history = new WeakHashMap<>(); 764 } 765 766 history.put(property, softReferenceDerivationLimit == 0 ? new WeakReference<>(newMap) : new SoftReference<>(newMap)); 767 } 768 769 /** 770 * Check the history for a map that already has the given property added. 771 * 772 * @param property {@link Property} to add. 773 * 774 * @return Existing map or {@code null} if not found. 775 */ checkHistory(final Property property)776 private PropertyMap checkHistory(final Property property) { 777 778 if (history != null) { 779 final Reference<PropertyMap> ref = history.get(property); 780 final PropertyMap historicMap = ref == null ? null : ref.get(); 781 782 if (historicMap != null) { 783 if (Context.DEBUG) { 784 historyHit.increment(); 785 } 786 787 return historicMap; 788 } 789 } 790 791 return null; 792 } 793 794 /** 795 * Returns true if the two maps have identical properties in the same order, but allows the properties to differ in 796 * their types. This method is mostly useful for tests. 797 * @param otherMap the other map 798 * @return true if this map has identical properties in the same order as the other map, allowing the properties to 799 * differ in type. 800 */ equalsWithoutType(final PropertyMap otherMap)801 public boolean equalsWithoutType(final PropertyMap otherMap) { 802 if (properties.size() != otherMap.properties.size()) { 803 return false; 804 } 805 806 final Iterator<Property> iter = properties.values().iterator(); 807 final Iterator<Property> otherIter = otherMap.properties.values().iterator(); 808 809 while (iter.hasNext() && otherIter.hasNext()) { 810 if (!iter.next().equalsWithoutType(otherIter.next())) { 811 return false; 812 } 813 } 814 815 return true; 816 } 817 818 @Override toString()819 public String toString() { 820 final StringBuilder sb = new StringBuilder(); 821 822 sb.append(Debug.id(this)); 823 sb.append(" = {\n"); 824 825 for (final Property property : getProperties()) { 826 sb.append('\t'); 827 sb.append(property); 828 sb.append('\n'); 829 } 830 831 sb.append('}'); 832 833 return sb.toString(); 834 } 835 836 @Override iterator()837 public Iterator<Object> iterator() { 838 return new PropertyMapIterator(this); 839 } 840 841 /** 842 * Check if this map contains properties with valid array keys 843 * 844 * @return {@code true} if this map contains properties with valid array keys 845 */ containsArrayKeys()846 public final boolean containsArrayKeys() { 847 return (flags & CONTAINS_ARRAY_KEYS) != 0; 848 } 849 850 /** 851 * Test to see if {@link PropertyMap} is extensible. 852 * 853 * @return {@code true} if {@link PropertyMap} can be added to. 854 */ isExtensible()855 boolean isExtensible() { 856 return (flags & NOT_EXTENSIBLE) == 0; 857 } 858 859 /** 860 * Test to see if {@link PropertyMap} is not extensible or any properties 861 * can not be modified. 862 * 863 * @return {@code true} if {@link PropertyMap} is sealed. 864 */ isSealed()865 boolean isSealed() { 866 return !isExtensible() && !anyConfigurable(); 867 } 868 869 /** 870 * Test to see if {@link PropertyMap} is not extensible or all properties 871 * can not be modified. 872 * 873 * @return {@code true} if {@link PropertyMap} is frozen. 874 */ isFrozen()875 boolean isFrozen() { 876 return !isExtensible() && allFrozen(); 877 } 878 879 /** 880 * Return a free field slot for this map, or {@code -1} if none is available. 881 * 882 * @return free field slot or -1 883 */ getFreeFieldSlot()884 int getFreeFieldSlot() { 885 if (freeSlots != null) { 886 final int freeSlot = freeSlots.nextSetBit(0); 887 if (freeSlot > -1 && freeSlot < fieldMaximum) { 888 return freeSlot; 889 } 890 } 891 if (fieldCount < fieldMaximum) { 892 return fieldCount; 893 } 894 return -1; 895 } 896 897 /** 898 * Get a free spill slot for this map. 899 * 900 * @return free spill slot 901 */ getFreeSpillSlot()902 int getFreeSpillSlot() { 903 if (freeSlots != null) { 904 final int freeSlot = freeSlots.nextSetBit(fieldMaximum); 905 if (freeSlot > -1) { 906 return freeSlot - fieldMaximum; 907 } 908 } 909 return spillLength; 910 } 911 912 /** 913 * Return a property map with the same layout that is associated with the new prototype object. 914 * 915 * @param newProto New prototype object to replace oldProto. 916 * @return New {@link PropertyMap} with prototype changed. 917 */ changeProto(final ScriptObject newProto)918 public synchronized PropertyMap changeProto(final ScriptObject newProto) { 919 final PropertyMap nextMap = checkProtoHistory(newProto); 920 if (nextMap != null) { 921 return nextMap; 922 } 923 924 if (Context.DEBUG) { 925 setProtoNewMapCount.increment(); 926 } 927 928 final PropertyMap newMap = makeUnsharedCopy(); 929 addToProtoHistory(newProto, newMap); 930 931 return newMap; 932 } 933 934 /** 935 * Make a copy of this property map with the shared prototype field set to null. Note that this is 936 * only necessary for shared maps of top-level objects. Shared prototype maps represented by 937 * {@link SharedPropertyMap} are automatically converted to plain property maps when they evolve. 938 * 939 * @return a copy with the shared proto map unset 940 */ makeUnsharedCopy()941 PropertyMap makeUnsharedCopy() { 942 final PropertyMap newMap = new PropertyMap(this); 943 newMap.sharedProtoMap = null; 944 return newMap; 945 } 946 947 /** 948 * Set a reference to the expected parent prototype map. This is used for class-like 949 * structures where we only want to use a top-level property map if all of the 950 * prototype property maps have not been modified. 951 * 952 * @param protoMap weak reference to the prototype property map 953 */ setSharedProtoMap(final SharedPropertyMap protoMap)954 void setSharedProtoMap(final SharedPropertyMap protoMap) { 955 sharedProtoMap = protoMap; 956 } 957 958 /** 959 * Get the expected prototype property map if it is known, or null. 960 * 961 * @return parent map or null 962 */ getSharedProtoMap()963 public PropertyMap getSharedProtoMap() { 964 return sharedProtoMap; 965 } 966 967 /** 968 * Returns {@code true} if this map has been used as a shared prototype map (i.e. as a prototype 969 * for a JavaScript constructor function) and has not had properties added, deleted or replaced since then. 970 * @return true if this is a valid shared prototype map 971 */ isValidSharedProtoMap()972 boolean isValidSharedProtoMap() { 973 return false; 974 } 975 976 /** 977 * Returns the shared prototype switch point, or null if this is not a shared prototype map. 978 * @return the shared prototype switch point, or null 979 */ getSharedProtoSwitchPoint()980 SwitchPoint getSharedProtoSwitchPoint() { 981 return null; 982 } 983 984 /** 985 * Return true if this map has a shared prototype map which has either been invalidated or does 986 * not match the map of {@code proto}. 987 * @param prototype the prototype object 988 * @return true if this is an invalid shared map for {@code prototype} 989 */ isInvalidSharedMapFor(final ScriptObject prototype)990 boolean isInvalidSharedMapFor(final ScriptObject prototype) { 991 return sharedProtoMap != null 992 && (!sharedProtoMap.isValidSharedProtoMap() || prototype == null || sharedProtoMap != prototype.getMap()); 993 } 994 995 /** 996 * {@link PropertyMap} iterator. 997 */ 998 private static class PropertyMapIterator implements Iterator<Object> { 999 /** Property iterator. */ 1000 final Iterator<Property> iter; 1001 1002 /** Current Property. */ 1003 Property property; 1004 1005 /** 1006 * Constructor. 1007 * 1008 * @param propertyMap {@link PropertyMap} to iterate over. 1009 */ PropertyMapIterator(final PropertyMap propertyMap)1010 PropertyMapIterator(final PropertyMap propertyMap) { 1011 iter = Arrays.asList(propertyMap.properties.getProperties()).iterator(); 1012 property = iter.hasNext() ? iter.next() : null; 1013 skipNotEnumerable(); 1014 } 1015 1016 /** 1017 * Ignore properties that are not enumerable. 1018 */ skipNotEnumerable()1019 private void skipNotEnumerable() { 1020 while (property != null && !property.isEnumerable()) { 1021 property = iter.hasNext() ? iter.next() : null; 1022 } 1023 } 1024 1025 @Override hasNext()1026 public boolean hasNext() { 1027 return property != null; 1028 } 1029 1030 @Override next()1031 public Object next() { 1032 if (property == null) { 1033 throw new NoSuchElementException(); 1034 } 1035 1036 final Object key = property.getKey(); 1037 property = iter.next(); 1038 skipNotEnumerable(); 1039 1040 return key; 1041 } 1042 1043 @Override remove()1044 public void remove() { 1045 throw new UnsupportedOperationException("remove"); 1046 } 1047 } 1048 1049 /* 1050 * Debugging and statistics. 1051 */ 1052 1053 /** 1054 * Debug helper function that returns the diff of two property maps, only 1055 * displaying the information that is different and in which map it exists 1056 * compared to the other map. Can be used to e.g. debug map guards and 1057 * investigate why they fail, causing relink 1058 * 1059 * @param map0 the first property map 1060 * @param map1 the second property map 1061 * 1062 * @return property map diff as string 1063 */ diff(final PropertyMap map0, final PropertyMap map1)1064 public static String diff(final PropertyMap map0, final PropertyMap map1) { 1065 final StringBuilder sb = new StringBuilder(); 1066 1067 if (map0 != map1) { 1068 sb.append(">>> START: Map diff"); 1069 boolean found = false; 1070 1071 for (final Property p : map0.getProperties()) { 1072 final Property p2 = map1.findProperty(p.getKey()); 1073 if (p2 == null) { 1074 sb.append("FIRST ONLY : [").append(p).append("]"); 1075 found = true; 1076 } else if (p2 != p) { 1077 sb.append("DIFFERENT : [").append(p).append("] != [").append(p2).append("]"); 1078 found = true; 1079 } 1080 } 1081 1082 for (final Property p2 : map1.getProperties()) { 1083 final Property p1 = map0.findProperty(p2.getKey()); 1084 if (p1 == null) { 1085 sb.append("SECOND ONLY: [").append(p2).append("]"); 1086 found = true; 1087 } 1088 } 1089 1090 //assert found; 1091 1092 if (!found) { 1093 sb.append(map0). 1094 append("!="). 1095 append(map1); 1096 } 1097 1098 sb.append("<<< END: Map diff\n"); 1099 } 1100 1101 return sb.toString(); 1102 } 1103 1104 // counters updated only in debug mode 1105 private static LongAdder count; 1106 private static LongAdder clonedCount; 1107 private static LongAdder historyHit; 1108 private static LongAdder protoInvalidations; 1109 private static LongAdder protoHistoryHit; 1110 private static LongAdder setProtoNewMapCount; 1111 static { 1112 if (Context.DEBUG) { 1113 count = new LongAdder(); 1114 clonedCount = new LongAdder(); 1115 historyHit = new LongAdder(); 1116 protoInvalidations = new LongAdder(); 1117 protoHistoryHit = new LongAdder(); 1118 setProtoNewMapCount = new LongAdder(); 1119 } 1120 } 1121 1122 /** 1123 * @return Total number of maps. 1124 */ getCount()1125 public static long getCount() { 1126 return count.longValue(); 1127 } 1128 1129 /** 1130 * @return The number of maps that were cloned. 1131 */ getClonedCount()1132 public static long getClonedCount() { 1133 return clonedCount.longValue(); 1134 } 1135 1136 /** 1137 * @return The number of times history was successfully used. 1138 */ getHistoryHit()1139 public static long getHistoryHit() { 1140 return historyHit.longValue(); 1141 } 1142 1143 /** 1144 * @return The number of times prototype changes caused invalidation. 1145 */ getProtoInvalidations()1146 public static long getProtoInvalidations() { 1147 return protoInvalidations.longValue(); 1148 } 1149 1150 /** 1151 * @return The number of times proto history was successfully used. 1152 */ getProtoHistoryHit()1153 public static long getProtoHistoryHit() { 1154 return protoHistoryHit.longValue(); 1155 } 1156 1157 /** 1158 * @return The number of times prototypes were modified. 1159 */ getSetProtoNewMapCount()1160 public static long getSetProtoNewMapCount() { 1161 return setProtoNewMapCount.longValue(); 1162 } 1163 } 1164