| // 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; | |
| /** | |
| * A Mutability is a resource associated with an {@link Environment} during an evaluation, | |
| * that gives those who possess it a revokable capability to mutate this Environment and | |
| * the objects created within that {@link Environment}. At the end of the evaluation, | |
| * the resource is irreversibly closed, at which point the capability is revoked, | |
| * and it is not possible to mutate either this {@link Environment} or these objects. | |
| * | |
| * <p>Evaluation in an {@link Environment} may thus mutate objects created in the same | |
| * {@link Environment}, but may not mutate {@link Freezable} objects (lists, sets, dicts) | |
| * created in a previous, concurrent or future {@link Environment}, and conversely, | |
| * the bindings and objects in this {@link Environment} will be protected from | |
| * mutation by evaluation in a different {@link Environment}. | |
| * | |
| * <p>In addition, a Mutability tracks which of its objects are temporarily locked from mutation. | |
| * This is used to prevent modification of iterables during loops. A 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, e.g. in the case of a list of lists. | |
| * | |
| * <p>Only a single thread may use the {@link Environment} and objects created within it while the | |
| * Mutability is still mutable as tested by {@link #isMutable}. Once the Mutability resource | |
| * is closed, the {@link Environment} and its objects are immutable and can be simultaneously used | |
| * by arbitrarily many threads. | |
| * | |
| * <p>The safe usage of a Mutability requires to always use try-with-resource style: | |
| * <code>try (Mutability mutability = Mutability.create(fmt, ...)) { ... }</code> | |
| * Thus, you can create a Mutability, build an {@link Environment}, mutate that {@link Environment} | |
| * and create mutable objects as you evaluate in that {@link Environment}, and finally return the | |
| * resulting {@link Environment}, at which point the resource is closed, and the {@link Environment} | |
| * and the objects it contains all become immutable. | |
| * (Unsafe usage is allowed only in 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 isMutable; | |
| // 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.isMutable = true; | |
| // 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)); | |
| } | |
| String getAnnotation() { | |
| return annotation; | |
| } | |
| @Override | |
| public String toString() { | |
| return String.format(isMutable ? "[%s]" : "(%s)", annotation); | |
| } | |
| boolean isMutable() { | |
| return isMutable; | |
| } | |
| /** | |
| * Return whether a {@link Freezable} belonging to this 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 (!isMutable) { | |
| return false; | |
| } | |
| return lockedItems.containsKey(object); | |
| } | |
| /** | |
| * For a locked {@link Freezable} that belongs to this Mutability, return a List of the | |
| * {@link Locations} 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 (!isMutable) { | |
| 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 (!isMutable) { | |
| // 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; | |
| isMutable = false; | |
| } | |
| /** | |
| * 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 {@link Freezable} object possesses a revokable Mutability attribute telling whether | |
| * the object is still mutable. All {@link Freezable} objects created in the same | |
| * {@link Environment} will share the same Mutability, inherited from this {@link Environment}. | |
| * Only evaluation in the same {@link Environment} is allowed to mutate these objects, | |
| * and only until the Mutability is irreversibly revoked. | |
| */ | |
| public interface Freezable { | |
| /** | |
| * Returns the {@link Mutability} capability 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().isMutable()) { | |
| // 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 argument to some function f2 evaluated in {@link environment} e2 | |
| // while e1 is be mutable, 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(); | |
| } |