Kotlin incorrectly uses getters for private fields in open classes

@InternalState
open class ImmutableIntIntArrayMap {
    private val map: LongArray
    @Cache private var lastEntryIndex = 0

...

    @Pure fun getLastEntry() = Entry(map[lastEntryIndex])
}

My code is using a Purity checker (GitHub - yairm210/Purity: Kotlin Compiler Plugin for validating Pure and Readonly functions), which enforces that @Pure methods do not call unmarked methods, or modify non-@Cache properties. Unfortunately, it fails this case:

Function “getLastEntry” is marked as Pure but calls non-Pure function “com.unciv.utils.ImmutableIntIntArrayMap.”.

  • If this is in your code, add @Pure annotation to the function.
  • If this is in external code, you can declare it as Pure via the PurityConfiguration in gradle - see Configuration - Purity
  • Since the function called is @Readonly, you can annotate “” as @Immutable so the call will be considered @Pure - see Advanced usage - Purity

After some experimentation, the crux appears to be a flaw in the Kotlin compiler rather than the purity checker. It appears that since this is an open class, the Kotlin compiler is generating a <get-lastEntryIndex> method for this field. But it shouldn’t be doing that, since this field is private.

Inheritance | Kotlin Documentation says “members are final…unless you explicitly mark them as open". A later paragraph says “If you override a member of a base class, the overriding member is also open by default.”, but (A) this doesn’t say it applies to fields, and (B) no class overrides this field. It also says “If you want to change this and forbid the subclasses of your class from overriding your implementation, you can explicitly mark the overriding member as final", but obviously I cannot do that since I need this field to be (privately) mutable.

So the generation of this <get-lastEntryIndex> method appears to be an error in the Kotlin compiler, with no obvious workaround for this specific case. This would normally be invisible, which is why nobody else has detected it, and is only visible via tools that work with the generated code, such as annotation processors.

1 Like

Have you tried changing it to @get:Cache? getters and setters are “generated” for all properties, but they’re optimized away later (in codegen I believe) for private properties

The @Cache annotation can only be applied to members, not methods, so I’m extremely dubious that syntax would work, as it appears to attempt to apply the annotation to the getter. I might be able to use @get::Purethough, to mark it as pure…