Skip to content

Latest commit

 

History

History
146 lines (99 loc) · 6.46 KB

disposable-iterators.md

File metadata and controls

146 lines (99 loc) · 6.46 KB

Disposable iterators

  • Type: Design proposal
  • Author: Andrey Breslav
  • Contributors: Vladimir Reshetnikov, Stanislav Erokhin
  • Status: Under consideration
  • Prototype: Not started

Feedback

Discussion of this proposal is held in this issue.

Synopsis

An iterator (for example, one generated by a coroutine) may iterate over a file or some other disposable resource. If the iteration completes normally (i.e. by reaching the last item), the iterator can dispose the underlying resource, but if

  • an exception occurs during one of the iterations,
  • break, continue or return cause early termination of the loop,

the resource will never be disposed.

To overcome this issue, C# has all for-loops wrapped in try/finally, where the finally block checks whether the iterator implements IDisposable and if so, calls the Dispose() method.

Here we propose the same for Kotlin.

References

Implementation

The idea is to wrap every for-loop that uses an iterator into a try/finally. The handler in the finally block is calling the following function (to be added to kotlin-runtime):

internal fun disposeIfNeeded(obj: Any?) {
    if (obj is Disposable) {
        obj.dispose()
    }
}

The aforementioned Disposable interface should be added to kotlin-runtime as follows:

package kotlin

public interface Disposable {
    fun dispose()
}

The Standard Library code should be revised and any iteration utilities there that iterate without using for-loops must be updated to provide the same semantics.

JVM costs

Apart from adding these two items to kotlin-runtime this results in generating extra byte code instructions for every for-loop that uses an iterator (note that loops that enumerate number ranges don't use iterators most of the time). This will amount to a minimum of 6 instructions per for-loop (+ TRYCATCHBLOCK entries in the Code Attribute). To be precise, this amounts to two instructions per copy of the finally block):

    ALOAD 1        # iterator
    INVOKESTATIC disposeIfNeeded(Ljava/lang/Object;)V

There are at least two copies of the finally block: one for normal termination, and another for exceptional termination. An extra copy is generated for each exit point, such as break, continue and return inside the loop.

Another two instructions need to be added for the implicit catch block (which has to be generated to implement the try/finally semantics): one to jump over the catch block in case of normal termination, another — to rethrow the exception in the catch block.

Here's the byte code for teh simplest case:

    ALOAD 0  ; iterable
    INVOKEINTERFACE java/lang/Iterable.iterator ()Ljava/util/Iterator;
    ASTORE 2 ; tmp_iterator
    
   START_TRY:
   LOOP:
    ALOAD 2  ; tmp_iterator
    INVOKEINTERFACE java/util/Iterator.hasNext ()Z
    IFEQ FINALLY
    
    ALOAD 2  ; tmp_iterator 
    INVOKEINTERFACE java/util/Iterator.next ()Ljava/lang/Object;
    ASTORE 1 ; loop_variable
    
    // loop body
    // ...
    
    GOTO LOOP
    
   FINALLY:
    ALOAD 2  ; tmp_iterator                                ; overhead
    INVOKESTATIC disposeIfNeeded (Ljava/lang/Object;)V     ; overhead
                                                           ; overhead
    GOTO AFTER_CATCH
   
   CATCH:
    ALOAD 2  ; tmp_iterator                                ; overhead
    INVOKESTATIC disposeIfNeeded (Ljava/lang/Object;)V     ; overhead
    
    ATHROW                                                 ; overhead
    
   AFTER_CATCH:
    // code after the loop
    // ...

Compatibility concerns

The code compiled with any pre-1.1 will run against the new library, but the for-loops there won't be disposing their iterators. This is not exactly a binary incompatibility: all code will keep working as before, but old clients for the new code will be unprepared, and won't hold their part of the deal (that is expected by the new code).

A minor and incomplete mitigation for this will be having the iterators dispose themselves when teh iteration completes normally, i.e. when hasNext() returns false for the first time. This won't help the case of an exceptions or early termination of a loop, though.

If we are ready to live with this, i.e. warn the users to recompile their old code, some new concerns arise:

  • the recompiled code will depend on the new kotlin-runtime,
  • recompiling the old code with Kotlin 1.1 may be undesirable for setup/compiler changes reasons.

This leads to thinking of adding this feature as a minimal change to Kotlin 1.0.X (under a flag, probably), and making it emit code that is tolerant to the old runtime (e.g. doesn't fail if disposeIfNeeded() or Disposable are missing).

The possible options here are:

  • Use Class.forName to check for presense of Disposable (do nothing if it's not present),
  • Catch NoSuchMethodError around the call to disposeIfNeeded() and swallow it.

Neither of these approaches will stop ProGuard from complaining.

Both these approaches are a bit costly when applied straightforwardly:

  • too many instructions,
  • time-consuming operations (class lookup or filling the stack trace for an error).

We can have fewer instructions by emitting a method that encapsulates this logic into

  • each module (troublesome for incremental compilation),
  • each file (more code).

We can mitigate time costs by caching the results: having a static flag for either whether Disposable is present, or disposeIfNeeded().

JS costs

TODO

Looks like an explicit try/finally has be generated around for-loops.

Arguments against this proposal

  • Maybe the compatibility issues are prohibitive for this feature?
  • Other ways of iterating, that do not use for-loops, won't become disposable-aware through this proposal
  • This is introducing a whole new concept of disposability into the ecosystem