// Copyright 2015 The Bazel Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.build.lib.syntax; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.events.Location; import com.google.devtools.build.lib.util.Preconditions; import java.io.Serializable; import java.util.ArrayList; import java.util.IdentityHashMap; import java.util.List; /** * Manages the capability to mutate an {@link Environment} and its contained Skylark objects. * * <p>Once the {@code Environment} is done evaluating, its {@code Mutability} is irreversibly closed * ("frozen"). At that point, it is no longer possible to mutate either the {@code Environment} or * its objects. This protects each {@code Environment} from unintentional and unsafe modification. * Before freezing, only a single thread may use the {@code Environment}, but after freezing, any * number of threads may access it. * * <p>It is illegal for an evaluation in one {@code Environment} to affect another {@code * Environment}, even if the second {@code Environment} has not yet been frozen. * * <p>A {@code Mutability} also tracks which {@link Freezable} objects in its {@code Environment} * are temporarily locked from mutation. This is used to prevent modification of iterables during * loops. A {@code Freezable} may be locked multiple times (e.g., nested loops over the same * iterable). Locking an object does not prohibit mutating its deeply contained values, such as in * the case of a list of lists. * * <p>To ensure safety, {@code Mutability}s must be created using the try-with-resource style: * <pre>{@code * try (Mutability mutability = Mutability.create(fmt, ...)) { ... } * }</pre> * The general pattern is to create a {@code Mutability}, build an {@code Environment}, mutate that * {@code Environment} and its objects, and possibly return the result from within the {@code try} * block, relying on the try-with-resource construct to ensure that everything gets frozen before * the result is used. The only code that should create a {@code Mutability} without using * try-with-resource is test code that is not part of the Bazel jar. */ // TODO(bazel-team): When we start using Java 8, this safe usage pattern can be enforced // through the use of a higher-order function. public final class Mutability implements AutoCloseable, Serializable { private boolean isFrozen; // For each locked Freezable, store all Locations where it is locked. // This field is set null once the Mutability is closed. This saves some // space, and avoids a concurrency bug from multiple Skylark modules // accessing the same Mutability at once. private IdentityHashMap<Freezable, List<Location>> lockedItems; private final String annotation; // For error reporting. /** * Creates a Mutability. * @param annotation an Object used for error reporting, * describing to the user the context in which this Mutability was active. */ private Mutability(String annotation) { this.isFrozen = false; // Seems unlikely that we'll often lock more than 10 things at once. this.lockedItems = new IdentityHashMap<>(10); this.annotation = Preconditions.checkNotNull(annotation); } /** * Creates a Mutability. * @param pattern is a {@link Printer#format} pattern used to lazily produce a string * for error reporting * @param arguments are the optional {@link Printer#format} arguments to produce that string */ public static Mutability create(String pattern, Object... arguments) { // For efficiency, we could be lazy and use formattable instead of format, // but the result is going to be serialized, anyway. return new Mutability(Printer.format(pattern, arguments)); } public String getAnnotation() { return annotation; } @Override public String toString() { return String.format(isFrozen ? "(%s)" : "[%s]", annotation); } public boolean isFrozen() { return isFrozen; } /** * Return whether a {@link Freezable} belonging to this {@code Mutability} is currently locked. * Frozen objects are not considered locked, though they are of course immutable nonetheless. */ public boolean isLocked(Freezable object) { if (!object.mutability().equals(this)) { throw new AssertionError("trying to check the lock of an object from a different context"); } if (isFrozen) { return false; } return lockedItems.containsKey(object); } /** * For a locked {@link Freezable} that belongs to this Mutability, return a List of the * {@link Location}s corresponding to its current locks. */ public List<Location> getLockLocations(Freezable object) { if (!isLocked(object)) { throw new AssertionError("trying to get lock locations for an object that is not locked"); } return lockedItems.get(object); } /** * Add a lock on a {@link Freezable} belonging to this Mutability. The object cannot be * mutated until all locks on it are gone. For error reporting purposes each lock is * associated with its originating {@link Location}. */ public void lock(Freezable object, Location loc) { if (!object.mutability().equals(this)) { throw new AssertionError("trying to lock an object from a different context"); } if (isFrozen) { return; } List<Location> locList; if (!lockedItems.containsKey(object)) { locList = new ArrayList<>(); lockedItems.put(object, locList); } else { locList = lockedItems.get(object); } locList.add(loc); } /** * Remove the lock for a given {@link Freezable} that is associated with the given * @{link Location}. It is an error if {@code object} does not belong to this mutability, * or has no lock corresponding to {@code loc}. */ public void unlock(Freezable object, Location loc) { if (!object.mutability().equals(this)) { throw new AssertionError("trying to unlock an object from a different context"); } if (isFrozen) { // It's okay if we somehow got frozen while there were still locked objects. return; } if (!lockedItems.containsKey(object)) { throw new AssertionError("trying to unlock an object that is not locked"); } List<Location> locList = lockedItems.get(object); boolean changed = locList.remove(loc); if (!changed) { throw new AssertionError(Printer.format("trying to unlock an object for a location at which " + "it was not locked (%r)", loc)); } if (locList.isEmpty()) { lockedItems.remove(object); } } /** * Freezes this Mutability, marking as immutable all {@link Freezable} objects that use it. */ @Override public void close() { // No need to track per-Freezable info since everything is immutable now. lockedItems = null; isFrozen = true; } /** * Freezes this Mutability * @return it in fluent style. */ public Mutability freeze() { close(); return this; } /** * A MutabilityException will be thrown when the user attempts to mutate an object he shouldn't. */ static class MutabilityException extends Exception { MutabilityException(String message) { super(message); } } /** * Each {@code Freezable} object possesses a {@link Mutability} that determines whether the object * is still mutable. All {@code Freezable} objects created in the same {@link Environment} will * share the same {@code Mutability}, inherited from this {@code Environment}. Only evaluation in * the same {@code Environment} is allowed to mutate these objects, and only until the * {@code Mutability} is irreversibly frozen. */ public interface Freezable { /** * Returns the {@link Mutability} associated with this Freezable object. * This should not change over the lifetime of the object. */ Mutability mutability(); } /** * Checks that this Freezable object can be mutated from the given {@link Environment}. * If the object is mutable, it must be from the environment. * @param object a Freezable object that we check is still mutable. * @param env the {@link Environment} attempting the mutation. * @throws MutabilityException when the object was frozen already, or is locked. */ public static void checkMutable(Freezable object, Environment env) throws MutabilityException { if (object.mutability().isFrozen()) { // Throw MutabilityException, not AssertionError, even if the object was from // another context. throw new MutabilityException("trying to mutate a frozen object"); } // Consider an {@link Environment} e1, in which is created {@link UserDefinedFunction} f1, // that closes over some variable v1 bound to list l1. If somehow, via the magic of callbacks, // f1 or l1 is passed as an argument to some function f2 evaluated in {@link Environment} e2 // while e1 is still mutable, then e2, being a different {@link Environment}, should not be // allowed to mutate objects from e1. It's a bug, that shouldn't happen in our current code // base, so we throw an AssertionError. If in the future such situations are allowed to happen, // then we should throw a MutabilityException instead. if (!object.mutability().equals(env.mutability())) { throw new AssertionError("trying to mutate an object from a different context"); } if (env.mutability().isLocked(object)) { Iterable<String> locs = Iterables.transform(env.mutability().getLockLocations(object), new Function<Location, String>() { @Override public String apply(Location loc) { return loc.print(); }}); throw new MutabilityException( "trying to mutate a locked object (is it currently being iterated over by a for loop " + "or comprehension?)\n" + "Object locked at the following location(s): " + String.join(", ", locs)); } } public static final Mutability IMMUTABLE = create("IMMUTABLE").freeze(); }