diff --git a/IMMUTABILITY.md b/IMMUTABILITY.md new file mode 100644 index 00000000000..c2080b3c958 --- /dev/null +++ b/IMMUTABILITY.md @@ -0,0 +1,24 @@ +# Immutability in Kotlin/Native + + Kotlin/Native implements strict mutability checks, ensuring +important invariant that object is either immutable or +accessible from the single thread at the moment (`mutable XOR global`). + + Immutability is the runtime property in Kotlin/Native, and can be applied +to an arbitrary object subgraph using `konan.worker.freeze` function. +It makes all objects reachable from the given one immutable, and +such a transition is a one way operation (object cannot be unfrozen later). +Some naturally immutable objects, such as `kotlin.String`, `kotlin.Int` and +other primitive types, along with `AtomicInt` and `AtomicReference` are frozen +by default. If mutating operation is applied to a frozen object, +an `InvalidMutabilityException` is thrown. + + To achieve `mutable XOR global` invariant all globally visible state (currently, +`object` singletons and enums) are automatically frozen. If an object freezing +is not desirable, `konan.ThreadLocal` annotation could be used, which will make +object state thread local, and thus, mutable (but changed state not visible to +other threads). + + Class `AtomicReference` could be used to publish changed frozen state to +other threads, and thus build patterns like shared caches. + diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt index 5207cc1ce30..68ff5709153 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/CodeGenerator.kt @@ -686,7 +686,7 @@ internal class FunctionGenerationContext(val function: LLVMValueRef, positionAtEnd(bbInit) val typeInfo = codegen.typeInfoForAllocation(descriptor) - val defaultConstructor = descriptor.constructors.first { it.valueParameters.size == 0 } + val defaultConstructor = descriptor.constructors.single { it.valueParameters.size == 0 } val ctor = codegen.llvmFunction(defaultConstructor) val (initFunction, args) = if (shared) { diff --git a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt index b2f9c43a611..6adadd41d58 100644 --- a/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt +++ b/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/llvm/IrToBitcode.kt @@ -38,13 +38,19 @@ import org.jetbrains.kotlin.ir.symbols.* import org.jetbrains.kotlin.ir.types.* import org.jetbrains.kotlin.ir.util.defaultType import org.jetbrains.kotlin.ir.util.getArguments +import org.jetbrains.kotlin.ir.util.hasAnnotation import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid import org.jetbrains.kotlin.ir.visitors.acceptVoid import org.jetbrains.kotlin.konan.target.CompilerOutputKind +import org.jetbrains.kotlin.name.FqName +import org.jetbrains.kotlin.name.Name import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameUnsafe -val IrClassSymbol.objectIsShared get() = owner.origin == DECLARATION_ORIGIN_ENUM +private val threadLocalAnnotationFqName = FqName("konan.ThreadLocal") + +val IrClassSymbol.objectIsShared get() = + !owner.hasAnnotation(threadLocalAnnotationFqName) internal fun emitLLVM(context: Context) { val irModule = context.irModule!! @@ -2662,4 +2668,4 @@ internal class CodeGeneratorVisitor(val context: Context, val lifetimes: Map { + Immutable.x++ + } + assertEquals(1, Immutable.x) + Mutable.x++ + assertEquals(3, Mutable.x) + println("OK") +} diff --git a/runtime/src/main/cpp/Arrays.cpp b/runtime/src/main/cpp/Arrays.cpp index 9937125ed32..2bed4af501e 100644 --- a/runtime/src/main/cpp/Arrays.cpp +++ b/runtime/src/main/cpp/Arrays.cpp @@ -28,7 +28,7 @@ namespace { ALWAYS_INLINE inline void mutabilityCheck(KConstRef thiz) { // TODO: optimize it! if (thiz->container()->frozen()) { - ThrowInvalidMutabilityException(); + ThrowInvalidMutabilityException(thiz); } } diff --git a/runtime/src/main/cpp/Atomic.cpp b/runtime/src/main/cpp/Atomic.cpp index c67d52ff222..8b7b24be27e 100644 --- a/runtime/src/main/cpp/Atomic.cpp +++ b/runtime/src/main/cpp/Atomic.cpp @@ -16,6 +16,7 @@ #include "Atomic.h" #include "Common.h" +#include "Exceptions.h" #include "Memory.h" #include "Types.h" @@ -44,8 +45,6 @@ inline AtomicReferenceLayout* asAtomicReference(KRef thiz) { extern "C" { -RUNTIME_NORETURN void ThrowInvalidMutabilityException(); - KInt Kotlin_AtomicInt_addAndGet(KRef thiz, KInt delta) { return addAndGetImpl(thiz, delta); } @@ -68,7 +67,7 @@ KNativePtr Kotlin_AtomicNativePtr_compareAndSwap(KRef thiz, KNativePtr expectedV void Kotlin_AtomicReference_checkIfFrozen(KRef value) { if (value != nullptr && !value->container()->permanentOrFrozen()) { - ThrowInvalidMutabilityException(); + ThrowInvalidMutabilityException(value); } } diff --git a/runtime/src/main/cpp/Exceptions.h b/runtime/src/main/cpp/Exceptions.h index d69e0a218e9..654b5c4364d 100644 --- a/runtime/src/main/cpp/Exceptions.h +++ b/runtime/src/main/cpp/Exceptions.h @@ -51,7 +51,7 @@ void RUNTIME_NORETURN ThrowNotImplementedError(); // Throws illegal character conversion exception (used in UTF8/UTF16 conversions). void RUNTIME_NORETURN ThrowIllegalCharacterConversionException(); void RUNTIME_NORETURN ThrowIllegalArgumentException(); -void RUNTIME_NORETURN ThrowInvalidMutabilityException(); +void RUNTIME_NORETURN ThrowInvalidMutabilityException(KConstRef where); // Prints out mesage of Throwable. void PrintThrowable(KRef); diff --git a/runtime/src/main/cpp/Memory.cpp b/runtime/src/main/cpp/Memory.cpp index 2722444ff60..de002dfa648 100644 --- a/runtime/src/main/cpp/Memory.cpp +++ b/runtime/src/main/cpp/Memory.cpp @@ -420,7 +420,6 @@ RUNTIME_USED ContainerHeader theStaticObjectsContainer = { void objc_release(void* ptr); void Kotlin_ObjCExport_releaseAssociatedObject(void* associatedObject); RUNTIME_NORETURN void ThrowFreezingException(); -RUNTIME_NORETURN void ThrowInvalidMutabilityException(); } // extern "C" @@ -1175,7 +1174,28 @@ OBJ_GETTER(InitInstance, OBJ_GETTER(InitSharedInstance, ObjHeader** location, ObjHeader** localLocation, const TypeInfo* type_info, void (*ctor)(ObjHeader*)) { #if KONAN_NO_THREADS - RETURN_RESULT_OF(InitInstance, location, type_info, ctor); + ObjHeader* value = *location; + if (value != nullptr) { + // OK'ish, inited by someone else. + RETURN_OBJ(value); + } + ObjHeader* object = AllocInstance(type_info, OBJ_RESULT); + UpdateRef(location, object); +#if KONAN_NO_EXCEPTIONS + ctor(object); + FreezeSubgraph(object); + return object; +#else + try { + ctor(object); + FreezeSubgraph(object); + return object; + } catch (...) { + UpdateRef(OBJ_RESULT, nullptr); + UpdateRef(location, nullptr); + throw; + } +#endif #else ObjHeader* value = *localLocation; if (value != nullptr) RETURN_OBJ(value); @@ -1188,28 +1208,27 @@ OBJ_GETTER(InitSharedInstance, // OK'ish, inited by someone else. RETURN_OBJ(value); } - ObjHeader* object = AllocInstance(type_info, OBJ_RESULT); - MEMORY_LOG("Calling UpdateRef from InitInstance\n") + RuntimeAssert(object->container()->normal() , "Shared object cannot be co-allocated"); + MEMORY_LOG("Calling UpdateRef from InitSharedInstance\n") UpdateRef(localLocation, object); #if KONAN_NO_EXCEPTIONS ctor(object); - if (!object->container()->frozen()) - ThrowFreezingException(); + FreezeSubgraph(object); UpdateRef(location, object); __sync_synchronize(); return object; #else try { ctor(object); - if (!object->container()->frozen()) - ThrowFreezingException(); + FreezeSubgraph(object); UpdateRef(location, object); __sync_synchronize(); return object; } catch (...) { UpdateRef(OBJ_RESULT, nullptr); UpdateRef(location, nullptr); + UpdateRef(localLocation, nullptr); __sync_synchronize(); throw; } @@ -1550,6 +1569,91 @@ void traverseStronglyConnectedComponent(ContainerHeader* container, } } +void freezeAcyclic(ContainerHeader* rootContainer) { + KStdDeque queue; + queue.push_back(rootContainer); + while (!queue.empty()) { + ContainerHeader* current = queue.front(); + queue.pop_front(); + current->unMark(); + current->resetBuffered(); + current->setColor(CONTAINER_TAG_GC_BLACK); + // Note, that once object is frozen, it could be concurrently accessed, so + // color and similar attributes shall not be used. + current->freeze(); + traverseContainerReferredObjects(current, [current, &queue](ObjHeader* obj) { + ContainerHeader* objContainer = obj->container(); + if (!objContainer->permanentOrFrozen()) { + if (objContainer->marked()) + queue.push_back(objContainer); + } + }); + } +} + +void freezeCyclic(ContainerHeader* rootContainer, const KStdVector& order) { + KStdUnorderedMap> reversedEdges; + KStdDeque queue; + queue.push_back(rootContainer); + while (!queue.empty()) { + ContainerHeader* current = queue.front(); + queue.pop_front(); + current->unMark(); + reversedEdges.emplace(current, KStdVector(0)); + traverseContainerReferredObjects(current, [current, &queue, &reversedEdges](ObjHeader* obj) { + ContainerHeader* objContainer = obj->container(); + if (!objContainer->permanentOrFrozen()) { + if (objContainer->marked()) + queue.push_back(objContainer); + reversedEdges.emplace(objContainer, KStdVector(0)).first->second.push_back(current); + } + }); + } + + KStdVector> components; + MEMORY_LOG("Condensation:\n"); + // Enumerate in the topological order. + for (auto it = order.rbegin(); it != order.rend(); ++it) { + auto* container = *it; + if (container->marked()) continue; + KStdVector component; + traverseStronglyConnectedComponent(container, reversedEdges, component); + MEMORY_LOG("SCC:\n"); + #if TRACE_MEMORY + for (auto c : component) + konan::consolePrintf(" %p\n", c); + #endif + components.push_back(std::move(component)); + } + + // Enumerate strongly connected components in reversed topological order. + for (auto it = components.rbegin(); it != components.rend(); ++it) { + auto& component = *it; + int internalRefsCount = 0; + int totalCount = 0; + for (auto* container : component) { + totalCount += container->refCount(); + traverseContainerReferredObjects(container, [&internalRefsCount](ObjHeader* obj) { + if (!obj->container()->permanentOrFrozen()) + ++internalRefsCount; + }); + } + // Create fictitious container for the whole component. + auto superContainer = component.size() == 1 ? component[0] : AllocAggregatingFrozenContainer(component); + // Don't count internal references. + superContainer->setRefCount(totalCount - internalRefsCount); + + // Freeze component. + for (auto* container : component) { + container->resetBuffered(); + container->setColor(CONTAINER_TAG_GC_BLACK); + // Note, that once object is frozen, it could be concurrently accessed, so + // color and similar attributes shall not be used. + container->freeze(); + } + } +} + /** * Theory of operations. * @@ -1570,12 +1674,12 @@ void traverseStronglyConnectedComponent(ContainerHeader* container, * incoming references from the same strongly connected component are not counted) * - mark all object's headers as frozen * - * Further reference counting on frozen objects is performed with the atomic operations, and so frozen - * references could be passed accross multiple threads. + * Further reference counting on frozen objects is performed with atomic operations, and so frozen + * references could be passed across multiple threads. */ void FreezeSubgraph(ObjHeader* root) { - // TODO: for now, we just check that passed object graph has no cycles, and throw an exception, - // if it does. Next version will run Kosoraju-Sharir if cycles are found. + // First check that passed object graph has no cycles. + // If there are cycles - run graph condensation on cyclic graphs using Kosoraju-Sharir. ContainerHeader* rootContainer = root->container(); if (rootContainer->permanentOrFrozen()) return; @@ -1583,79 +1687,11 @@ void FreezeSubgraph(ObjHeader* root) { bool hasCycles = false; KStdVector order; depthFirstTraversal(rootContainer, &hasCycles, order); - - KStdUnorderedMap> reversedEdges; // Now unmark all marked objects, and freeze them, if no cycles detected. - KStdDeque queue; - queue.push_back(rootContainer); - while (!queue.empty()) { - ContainerHeader* current = queue.front(); - queue.pop_front(); - current->unMark(); - - if (hasCycles) { - reversedEdges.emplace(current, KStdVector(0)); - } else { - current->resetBuffered(); - current->setColor(CONTAINER_TAG_GC_BLACK); - // Note, that once object is frozen, it could be concurrently accessed, so - // color and similar attributes shall not be used. - current->freeze(); - } - traverseContainerReferredObjects(current, [hasCycles, current, &queue, &reversedEdges](ObjHeader* obj) { - ContainerHeader* objContainer = obj->container(); - if (!objContainer->permanentOrFrozen()) { - if (objContainer->marked()) - queue.push_back(objContainer); - if (hasCycles) - reversedEdges.emplace(objContainer, KStdVector(0)).first->second.push_back(current); - } - }); - } - if (hasCycles) { - KStdVector> components; - MEMORY_LOG("Condensation:\n"); - // Enumerate in topological order. - for (auto it = order.rbegin(); it != order.rend(); ++it) { - auto* container = *it; - if (container->marked()) continue; - KStdVector component; - traverseStronglyConnectedComponent(container, reversedEdges, component); - MEMORY_LOG("SCC:\n"); -#if TRACE_MEMORY - for (auto c : component) - konan::consolePrintf(" %p\n", c); -#endif - components.push_back(std::move(component)); - } - // Enumerate strongly connected components in reversed topological order. - for (auto it = components.rbegin(); it != components.rend(); ++it) { - auto& component = *it; - int internalRefsCount = 0; - int totalCount = 0; - for (auto* container : component) { - totalCount += container->refCount(); - traverseContainerReferredObjects(container, [&internalRefsCount](ObjHeader* obj) { - if (!obj->container()->permanentOrFrozen()) - ++internalRefsCount; - }); - } - auto superContainer = component.size() == 1 - ? component[0] - : AllocAggregatingFrozenContainer(component); // Create fictitious container for the whole component. - // Don't count internal references. - superContainer->setRefCount(totalCount - internalRefsCount); - - // Freeze component. - for (auto* container : component) { - container->resetBuffered(); - container->setColor(CONTAINER_TAG_GC_BLACK); - // Note, that once object is frozen, it could be concurrently accessed, so - // color and similar attributes shall not be used. - container->freeze(); - } - } + freezeCyclic(rootContainer, order); + } else { + freezeAcyclic(rootContainer ); } // Now remove frozen objects from the toFree list. @@ -1671,14 +1707,14 @@ void FreezeSubgraph(ObjHeader* root) { // This function is called from field mutators to check if object's header is frozen. // If object is frozen, an exception is thrown. void MutationCheck(ObjHeader* obj) { - if (obj->container()->frozen()) ThrowInvalidMutabilityException(); + if (obj->container()->frozen()) ThrowInvalidMutabilityException(obj); } OBJ_GETTER(SwapRefLocked, ObjHeader** location, ObjHeader* expectedValue, ObjHeader* newValue, int32_t* spinlock) { lock(spinlock); ObjHeader* oldValue = *location; - // We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under lock. + // We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock. if (oldValue == expectedValue) { SetRef(location, newValue); } else { @@ -1697,7 +1733,7 @@ OBJ_GETTER(SwapRefLocked, OBJ_GETTER(ReadRefLocked, ObjHeader** location, int32_t* spinlock) { lock(spinlock); ObjHeader* value = *location; - // We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under lock. + // We do not use UpdateRef() here to avoid having ReleaseRef() on return slot under the lock. if (value != nullptr) AddRef(value); unlock(spinlock); diff --git a/runtime/src/main/cpp/Memory.h b/runtime/src/main/cpp/Memory.h index 60c80f7b7f3..481cf38f0d1 100644 --- a/runtime/src/main/cpp/Memory.h +++ b/runtime/src/main/cpp/Memory.h @@ -67,6 +67,10 @@ struct ContainerHeader { // Number of objects in the container. uint32_t objectCount_; + inline bool normal() const { + return (refCount_ & CONTAINER_TAG_MASK) == CONTAINER_TAG_NORMAL; + } + inline bool permanent() const { return (refCount_ & CONTAINER_TAG_MASK) == CONTAINER_TAG_PERMANENT; } diff --git a/runtime/src/main/kotlin/konan/Annotations.kt b/runtime/src/main/kotlin/konan/Annotations.kt index 99167412390..11940913a1f 100644 --- a/runtime/src/main/kotlin/konan/Annotations.kt +++ b/runtime/src/main/kotlin/konan/Annotations.kt @@ -36,7 +36,7 @@ annotation class SymbolName(val name: String) annotation class ExportTypeInfo(val name: String) /** - * * If lambda shall be carefully lowered by the compiler. + * If a lambda shall be carefully lowered by the compiler. */ annotation class VolatileLambda @@ -61,36 +61,6 @@ public annotation class Used @Retention(AnnotationRetention.SOURCE) public annotation class Throws(vararg val exceptionClasses: KClass) -/** - * Need to be fixed because of reification support. - */ -public annotation class FixmeReified - -/** - * Need to be fixed because of sorting support. - */ -public annotation class FixmeSorting - -/** - * Need to be fixed because of specialization support. - */ -public annotation class FixmeSpecialization - -/** - * Need to be fixed because of sequences support. - */ -public annotation class FixmeSequences - -/** - * Need to be fixed because of variance support. - */ -public annotation class FixmeVariance - -/** - * Need to be fixed because of regular expressions. - */ -public annotation class FixmeRegex - /** * Need to be fixed because of reflection. */ @@ -101,18 +71,29 @@ public annotation class FixmeReflection */ public annotation class FixmeConcurrency -public annotation class FixmeInline +/** + * Need to be fixed because of header/impl notation + */ +public annotation class FixmeMultiplatform /** - * Need to be fixed. + * Need to be fixed because of random support. */ -public annotation class Fixme +public annotation class FixmeRandom +/** + * Escape analysis annotations. + */ public annotation class Escapes(val who: Int) public annotation class PointsTo(vararg val onWhom: Int) /** - * Need to be fixed because of header/impl notation + * Top level variable or object is thread local, and so could be mutable. + * One may use this annotation as the stopgap measure for singleton + * object immutability. + * PLEASE NOTE THAT THIS ANNOTATION MAY GO AWAY IN UPCOMING RELEASES. */ -public annotation class FixmeMultiplatform +@Target(AnnotationTarget.FIELD, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class ThreadLocal \ No newline at end of file diff --git a/runtime/src/main/kotlin/konan/worker/Freezing.kt b/runtime/src/main/kotlin/konan/worker/Freezing.kt index 40e14cd90d4..67f4b0feea3 100644 --- a/runtime/src/main/kotlin/konan/worker/Freezing.kt +++ b/runtime/src/main/kotlin/konan/worker/Freezing.kt @@ -26,7 +26,8 @@ public class FreezingException() : RuntimeException() /** * Exception thrown whenever we attempt to mutate frozen objects. */ -public class InvalidMutabilityException() : RuntimeException() +public class InvalidMutabilityException(where: Any) : + RuntimeException("mutation attempt of frozen $where (hash is 0x${where.hashCode().toString(16)})") /** * Freezes object subgraph reachable from this object. Frozen objects can be freely @@ -50,4 +51,4 @@ internal external fun isFrozenInternal(it: Any?): Boolean internal fun ThrowFreezingException(): Nothing = throw FreezingException() @ExportForCppRuntime -internal fun ThrowInvalidMutabilityException(): Nothing = throw InvalidMutabilityException() +internal fun ThrowInvalidMutabilityException(where: Any): Nothing = throw InvalidMutabilityException(where) diff --git a/runtime/src/main/kotlin/kotlin/collections/Collections.kt b/runtime/src/main/kotlin/kotlin/collections/Collections.kt index efab69e7bb7..2295f9f21c1 100644 --- a/runtime/src/main/kotlin/kotlin/collections/Collections.kt +++ b/runtime/src/main/kotlin/kotlin/collections/Collections.kt @@ -18,10 +18,7 @@ package kotlin.collections import kotlin.comparisons.* - -// copies typed varargs array to array of objects -// TODO: generally wrong, wrt specialization. -@FixmeSpecialization +// Copies typed varargs array to an array of objects internal actual fun Array.copyToArrayOfAny(isVarargs: Boolean): Array = if (isVarargs) // if the array came from varargs and already is array of Any, copying isn't required. @@ -232,6 +229,7 @@ public actual fun MutableList.fill(value: T): Unit { * * See: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm */ +@FixmeRandom @SinceKotlin("1.2") public actual fun MutableList.shuffle(): Unit { for (i in lastIndex downTo 1) { diff --git a/runtime/src/main/kotlin/kotlin/coroutines/experimental/Intrinsics.kt b/runtime/src/main/kotlin/kotlin/coroutines/experimental/Intrinsics.kt index 49a3537e96f..50b2286cdc9 100644 --- a/runtime/src/main/kotlin/kotlin/coroutines/experimental/Intrinsics.kt +++ b/runtime/src/main/kotlin/kotlin/coroutines/experimental/Intrinsics.kt @@ -64,11 +64,9 @@ public actual fun (suspend R.() -> T).createCoroutineUnchecked( (this.create(receiver, completion) as CoroutineImpl).facade // INTERNAL DEFINITIONS -// TODO: uncomment as soon as inlining works for stdlib. -@FixmeInline -private /*inline*/ fun buildContinuationByInvokeCall( +private inline fun buildContinuationByInvokeCall( completion: Continuation, - /*crossinline*/ block: () -> Any? + crossinline block: () -> Any? ): Continuation { val continuation = object : Continuation {