// 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.Preconditions; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.events.Location; import java.io.Serializable; import java.util.ArrayList; import java.util.Formattable; import java.util.IdentityHashMap; import java.util.List; /** * An object that manages the capability to mutate Skylark objects and their {@link Environment}s. * Collectively, the managed objects are called {@link Freezable}s. * *
Each {@code Environment}, and each of the mutable Skylark values (i.e., {@link * SkylarkMutable}s) that are created in that {@code Environment}, holds a pointer to the same * {@code Mutability} instance. Once the {@code Environment} is done evaluating, its {@code * Mutability} is irreversibly closed ("frozen"). At that point, it is no longer possible to change * either the bindings in that {@code Environment} or the state of its objects. This protects each * {@code Environment} from unintentional and unsafe modification. * *
{@code Mutability}s enforce isolation between {@code Environment}s; it is illegal for an * evaluation in one {@code Environment} to affect the bindings or values of another. In particular, * the {@code Environment} for any Skylark module is frozen before its symbols can be imported for * use by another module. Each individual {@code Environment}'s evaluation is single-threaded, so * this isolation also translates to thread safety. Any number of threads may simultaneously access * frozen data. (The {@code Mutability} itself is also thread-safe if and only if it is frozen.} * *
Although the mutability pointer of a {@code Freezable} contains some debugging information * about its context, this should not affect the {@code Freezable}'s semantics. From a behavioral * point of view, the only thing that matters is whether the {@code Mutability} is frozen, not what * particular {@code Mutability} object is pointed to. * *
A {@code Mutability} also tracks which {@code 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. * *
We follow two disciplines to ensure safety. First, all mutation methods of a {@code Freezable} * must take in a {@code Mutability} as a parameter, and confirm that *
Second, {@code Mutability}s are created using the try-with-resource style: *
{@code * try (Mutability mutability = Mutability.create(fmt, ...)) { ... } * }* 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. * *
We keep some (unchecked) invariants regarding where {@code Mutability} objects may appear * within a compound value. *
There is a special API for freezing individual values rather than whole {@code Environment}s.
* Because this API makes it easier to violate the above invariants, you should avoid using it if at
* all possible; at the moment it is only used for serialization. Under this API, you may call
* {@link Freezable#unsafeShallowFreeze} to reset a value's {@code Mutability} pointer to be {@link
* #IMMUTABLE}. This operation has no effect on the {@code Mutability} itself. It is up to the
* caller to preserve or restore the above invariants by ensuring that any deeply contained values
* are also frozen. For safety and explicitness, this operation is disallowed unless the {@code
* Mutability}'s {@link #allowsUnsafeShallowFreeze} method returns true.
*/
public final class Mutability implements AutoCloseable, Serializable {
/**
* If true, mutation of any {@link Freezable} associated with this {@code Mutability} is
* disallowed.
*/
private boolean isFrozen;
/**
* For each locked {@link Freezable}, stores all {@link Location}s where it is locked.
*
* This field is set null once the {@code Mutability} is closed. This saves some space, and avoids
* a concurrency bug from multiple Skylark modules accessing the same {@code Mutability} at once.
*/
private IdentityHashMap This method is optional (i.e. may throw {@link NotImplementedException}).
*
* If this object's {@link Mutability} is 1) not frozen, and 2) has {@link
* #allowUnsafeShallowFreeze} return true, then the object's {@code Mutability} reference is
* updated to point to {@link #IMMUTABLE}. Otherwise, this method throws {@link
* IllegalArgumentException}.
*
* It is up to the caller to ensure that any contents of this {@code Freezable} are also
* frozen in order to preserve/restore the invariant that an immutable value cannot contain a
* mutable one unless the immutable value's {@code Mutability} is {@link #SHALLOW_IMMUTABLE}.
* Note that {@link SkylarkMutable#isImmutable} correctness and thread-safety are not guaranteed
* otherwise.
*/
default void unsafeShallowFreeze() {
throw new UnsupportedOperationException();
}
/**
* Throws {@link IllegalArgumentException} if the precondition for {@link #unsafeShallowFreeze}
* is violated. To be used by implementors of {@link #unsafeShallowFreeze}.
*/
static void checkUnsafeShallowFreezePrecondition(
Freezable freezable) {
Mutability mutability = freezable.mutability();
if (mutability.isFrozen()) {
// It's not safe to rewrite the Mutability pointer if this is already frozen, because we
// could be accessed by multiple threads.
throw new IllegalArgumentException(
"cannot call unsafeShallowFreeze() on an object whose Mutability is already frozen");
}
if (!mutability.allowsUnsafeShallowFreeze()) {
throw new IllegalArgumentException(
"cannot call unsafeShallowFreeze() on a mutable object whose Mutability's "
+ "allowsUnsafeShallowFreeze() == false");
}
}
}
/**
* Checks that the given {@code Freezable} can be mutated using the given {@code Mutability}, and
* throws an exception if it cannot.
*
* @throws MutabilityException if the object is either frozen or locked
* @throws IllegalArgumentException if the given {@code Mutability} is not the same as the one
* the {@code Freezable} is associated with
*/
public static void checkMutable(Freezable object, Mutability mutability)
throws MutabilityException {
if (object.mutability().isFrozen()) {
// Throw MutabilityException, not IllegalArgumentException, 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 IllegalArgumentException. If in the future such situations are allowed to happen,
// then we should throw a MutabilityException instead.
if (!object.mutability().equals(mutability)) {
throw new IllegalArgumentException("trying to mutate an object from a different context");
}
if (mutability.isLocked(object)) {
Iterable It is not associated with any particular {@link Environment}.
*/
public static final Mutability IMMUTABLE = create("IMMUTABLE").freeze();
/**
* A {@code Mutability} indicating that a value is shallowly immutable.
*
* Under the invariants for this class, this is the only frozen {@code Mutability} whose values
* are permitted to directly or indirectly contain mutable values.
*
* In practice, this instance is used as the {@code Mutability} for tuples.
*/
// TODO(bazel-team): We might be able to remove this instance, and instead have tuples and other
// immutable types store the same Mutability as other values in that environment. Then we can
// simplify the Mutability invariant, and implement deep-immutability checking in constant time
// for values whose Environments have been frozen.
//
// This would also affect structs (SkylarkInfo). Maybe they would implement an interface similar
// to SkylarkMutable, or the relevant methods could be worked into SkylarkValue.
public static final Mutability SHALLOW_IMMUTABLE = create("SHALLOW_IMMUTABLE").freeze();
}