DIY Coroutines. Part 1. Lazy Generators

In the JVM world, coroutines are best known thanks to the Kotlin language and Project Loom. I have not seen a good description of the principle of the Kotlinovsky coroutines, and the code of the kotlin-coroutines library is completely incomprehensible to an unprepared person. In my experience, most people only know about coroutines that these are “lightweight streams” and that in kotlin they work through smart bytecode generation. So I was until recently. And the idea came to me that since coroutines can be implemented in bytecode, why not implement them in java. From this idea, subsequently, a small and fairly simple library appeared, the device of which, I hope, can be understood by almost any developer. Details under the cut.

Source

I called the project Microutines, which came from the words Micro and Coroutines.

All code is available on github. Initially, I wanted to build the narrative in accordance with the development of my own understanding of the topic, to talk about my wrong decisions and ideas, about the development of api, but then the code in the article would be very different from the code with github (which is inevitable in any case, I write one article times, and on github comedic from time to time). Therefore, I will mostly describe the final version of the library, and if some parts of the api are not very clear at first glance, then most likely they are needed to solve the problems that we will consider in the following articles.

Disclaimer

This is a kind of training project. I will be glad if one of the readers inclines him to play or even throw a pull request. But use it in production is not worth it. The best way to deeply understand a technology is to implement it yourself, which is the only goal of this project.

And I do not guarantee the accuracy of the terms used. Perhaps some of them I heard somewhere, incorrectly memorized, and some of them even came up with myself and forgot.

What are coroutines

As I have already noticed, it is often said about coroutines that these are “facilitated flows”. This is not a true definition. I will not give a true definition either, but I will try to describe what they are, coroutines. Calling coroutines flows will not be entirely correct. Coroutine is a smaller planning unit than a stream, and a stream, in turn, is smaller than a scheduling unit. Process and thread planning is handled by the operating system. Corutin is engaged in planning … we ourselves will be engaged in their planning. Coroutines work on top of regular threads, and their main trick is that they do not block the thread when they wait for some other task to be completed, but release it for another coroutine. This approach is called cooperative multitasking. Corutin can work first in one thread, then in another. A thread for coroutine acts as a resource, and a million coroutine can work on a single thread. You could see this picture:

The tasks of contribs 1 and contribs 2 fulfill some kind of requests and do not block the flow while waiting for responses, but suspend their work and continue it after receiving the answers. We can write such code using callbacks, you say. That is true, but the essence of coroutine is that we write code without callbacks, we write ordinary sequential code that runs asynchronously.

Generators

We’ll start development from simple to complex. The first thing we will do is lazy collection generation, implemented in some languages ​​using the yield keyword. Generators are not accidental here, as we will see later, and generators and coroutines can be implemented using the same mechanism.

Let’s consider an example in python, simply because the generators are there out of the box.

def generator():
    k = 10
    yield k
    k += 10
    yield k
    k += 10
    yield k

for i in generator():
    print(i)

The cycle unfolds into something like this (maybe not quite like that, but the principle is important to us):

gen = generator()
while True:
    try:
        i = next(gen)
        print(i)
    except StopIteration:
        break

Call generator() will create a special iterator called a generator. On first call next(gen) code is executed from the beginning of the function generator before the first yield, and to the variable i the value of the local variable is written k of genertator(). Every next call next will continue to execute the function with the instruction immediately following the previous one yield etc. Moreover, between calls next values ​​of all local variables are stored inside generator.

That’s about the same, but in the Kotlin language.

val seq = sequence {
    var i = 10 
    yield(i)
    i += 10
    yield(i)
    i += 10
    yield(i)
}

for (i in seq) {
    println(i)
}

In Java, we could do lazy generation something like this:

Iterable seq = DummySequence.first(() -> {
    final int i = 10;
    return DummySequence.next(i, () -> {
            final int i1 = i + 10;
            return DummySequence.next(i1, () -> DummySequence.end(i1 + 10));
    });
});

for(int i: seq) {
    System.out.println(i);
}

DummySequence implementation

import org.junit.Assert;
import org.junit.Test;

import java.util.Iterator;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

public class DummySequenceTest {

    @Test
    public void dummySequenceTest() {
        DummySequence sequence = DummySequence.first(() -> {
            final int i = 10;
            return DummySequence.next(10, () -> {
                final int i1 = i + 10;
                return DummySequence.next(i1, () -> DummySequence.end(i1 + 10));
            });
        });

        List list = StreamSupport.stream(sequence.spliterator(), false)
                .collect(Collectors.toList());

        Assert.assertEquals(10, ((int) list.get(0)));
        Assert.assertEquals(20, ((int) list.get(1)));
        Assert.assertEquals(30, ((int) list.get(2)));
    }

    private static class DummySequence implements Iterable, Iterator {
        private Step step;

        public DummySequence(Step step) {
            this.step = step;
        }

        @Override
        public Iterator iterator() {
            return this;
        }

        @Override
        public boolean hasNext() {
            if (step instanceof EndStep)
                return false;
            step = step.nextStep();
            return true;
        }

        @Override
        public T next() {
            return step.getValue();
        }

        public static  DummySequence first(Supplier> next) {
            return new DummySequence<>(new FirstStep(next));
        }

        public static  Step next(T value, Supplier> next) {
            return new IntermediateStep<>(value, next);
        }

        public static  Step end(T value) {
            return new EndStep<>(value);
        }
    }

    private interface Step {
        T getValue();

        Step nextStep();
    }

    public static class FirstStep implements Step {
        Supplier> nextStep;

        public FirstStep(Supplier> next) {
            this.nextStep = next;
        }

        @Override
        public T getValue() {
            throw new IllegalStateException();
        }

        @Override
        public Step nextStep() {
            return nextStep.get();
        }
    }

    public static class IntermediateStep implements Step {
        T value;
        Supplier> nextStep;

        public IntermediateStep(T value, Supplier> nextStep) {
            this.value = value;
            this.nextStep = nextStep;
        }

        @Override
        public T getValue() {
            return value;
        }

        @Override
        public Step nextStep() {
            return nextStep.get();
        }
    }

    public static class EndStep implements Step {
        T value;

        public EndStep(T value) {
            this.value = value;
        }

        @Override
        public T getValue() {
            return value;
        }

        @Override
        public Step nextStep() {
            throw new IllegalStateException();
        }
    }
}

Each next nested lambda captures all the variables from all previous lambdas and is executed only when the next element is requested. The result of each lambda will be the generated element and the next block of code. It looks very strange, and I doubt that anyone will write like that. We denote the ideal that we will strive for (and we will achieve it, except that we will use an anonymous class instead of lambdas).

Sequence sequence = new Sequence(() -> {
    int i = 10; 
    yield(i);
    i += 10;
    yield(i);
    i += 10;
    yield(i);
});

The function passed to the Sequence constructor must come from yield to yield only if necessary, the values ​​of local variables should be stored between calls sequence.next(). This is saving the stack and the number of the last executed instruction called crowding out (yield is translated into Russian) or suspense.

Continuation

A piece that can be squeezed out is called Continuation. Continuation is translated into Russian as ‘continuation’, but I will call it continuation. Wikipedia writes about continuation:

Continuation (Eng. Continuation) represents the state of the program at a certain moment, which can be saved and used to transition to this state. Continuations contain all the information to continue executing a program from a specific point.

Suppose that we already have some kind of magical way implemented the mechanism of continuations, which is represented by the following interface. Method run knows how to stop its execution. Each subsequent call resumes execution from the last yield. We can think of continuation as Runnable, which can be performed in parts.

interface Continuation {
    void run(SequenceScope scope);
}

We will use the continuation like this:

Sequence sequence = new Sequence<>(new Continuation<>() {
    void run(SequenceScope scope) {
        int i = 1;
        System.out.println("Continuation start");
        scope.yield(i++);
        System.out.println("Continuation resume");
        scope.yield(i++);
        System.out.println("Continuation resume");
        scope.yield(i++);
        System.out.println("Continuation end");
    }       
});     

for(Integer i: sequence) {
    System.out.println("Next element :" + i);
}

And we expect to get this conclusion:

Output

Continuation start
Next element: 1
Continuation resume
Next element: 2
Continuation resume
Next element: 3
Continuation end

Sequence when prompted for the next item will call Continuation.run(scope)which will execute a block of code until the next yield and get crowded out. Next challenge Continuation.run(scope) will start work from the place of the last crowding out and execute the code until the next yield. The code Sequence may be like this:

class Sequence implements Iterator, SequenceScope, Iterable {
    private static final Object STOP = new Object();
    private Object next = STOP;
    private Continuation nextStep;

    public Sequence(Continuation nextStep) {
        this.nextStep = nextStep;
    }

    @Override
    public boolean hasNext() {
        if (next == STOP) {
            nextStep.run(this);
        }
        return next != STOP;
    }

    @Override
    public T next() {
        if (next == STOP) {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
        }

        T result = (T) next;
        next = STOP;
        return result;
    }

    @Override
    void yield(T t) {
        next = t;
    }

    public Iterator iterator() { // сомнительная реализация, но сейчас это не имеет значения
        return this;
    }

}

interface SequenceScope {
    void yield(T t);
}

Everything is fine, except that java is not able to stop the execution of the method in an arbitrary place, so that then it can continue execution from the place of the last stop. Therefore, we will have to do this manually. We introduce the label field, in which we will store the number of the last called yield.

class IntegerSequenceContinuation implements Continuation {
    private int label = 0;
    private int i = 0;
    void run(SequenceScope scope) {
        int i = this.i;
        switch (label) {
            case 0:
                System.out.println("Continuation start");
                scope.yield(i++);
                label = 1;
                this.i = i;
                return;
            case 1:
                System.out.println("Continuation resume");
                scope.yield(i++);
                label = 2;
                this.i = i;
                return;
            case 2:
                System.out.println("Continuation resume");
                scope.yield(i++);
                label = 3;
                this.i = i;
                return;
            case 3:
                System.out.println("Continuation end");
                label = 4;
            default:
                throw new RuntimeException();
        }
    }
}

We got a state machine (finite state machine), and by and large this is exactly what Kotlin does in his coroutines (you can decompile and see if, of course, you understand something). We have 4 states, each call run executes part of the code and makes the transition to the next state. We have to save the local variable i in the class field. In addition to unjustified complexity, this code has another problem: we can pass different values ​​as the scope parameter for each run call. Therefore, it would be nice to save the scope parameter in the class field on the first call, and continue to work with it.

Continuation on java is implemented in us, but in a rather strange way and in only one instance. Each time no one will write something similar, editing such a code is difficult, reading such a code is difficult. Therefore, we will build the state machine after compilation.

Suspendable & Continuation

How do we understand if the continuation has completed work or has suspended? Let the method run returns a special object SUSPEND in case of suspension.

public interface Continuation {
    Object SUSPEND = new Object() {
        @Override
        public String toString() {
            return "[SUSPEND]";
        }
    };

    T run();
}

Note that I removed the input parameter from the continuation. We must ensure that the parameters do not change from call to call, the best way to do this is to delete them. The user, on the contrary, needs a parameter scope (it will be used for a lot of things, but now it is being transferred to its place SequenceScopefrom which our yield) In addition, not about any SUSPEND the user does not want to know and does not want to return anything. Introduce the interface Suspendable.

public abstract class Suspendable {
    abstract public void run(C scope);
}

interface Scope {}

Why an abstract class, not an interface?

Using a class instead of an interface does not allow writing lambdas and forces writing anonymous classes. It will be very convenient for us to work in bytecode with continuations as with classes, because local fields can be stored in their fields. But lambdas in bytecode do not look like a class. For details, go here.

Suspendable this is Continuation in design time, a Continuation this is Suspendable in runtime. The user writes code at the level of Suspendable, and the low-level library code works with Continuation. It turns into one after the modification of the bytecode.

Before we talked about crowding out after a call yield, but in the future we will need to be forced out after some other methods. We will mark such methods with an annotation @Suspend. This also applies to yield:

public class SequenceScope implements Scope {
    @Suspend
    public void yield(T t) {...}
}

Remember that our continuations will be built on finite automata. Let us dwell here in more detail. It is called a finite state machine because it has a finite number of states. To store the current state, we will use a special label field. Originally поле label is 0 – zero (initial) state. Every challenge Continuation.run will execute some code and go into some state (in any other than the initial one). After each transition, the continuation must save all local variables, the current state number, and execute return SUSPEND. The transition to the final state will be indicated return null (in the following articles we will return not only null) Call Continuation.run from the final state should end with an exception ContinuationEndException.

So, the user writes the code in Suspendable, after compilation it turns into Continuation, with which the library works, and, in particular, our generators. Creating a new generator for the user looks like this:

Sequence seq = new Sequence(new Suspendable() {...});

But the generator itself needs continuation, because he needs to initialize the field Continuation nextStep;. For getting Continuation of Suspendable I wrote a special class in code Magic.

package joroutine.core;

import joroutine.coroutine.CoroutineScopeImpl;

import java.lang.reflect.Field;

public class Magic {
    public static final String SCOPE = "scope$S";

    private static  Continuation createContinuation(Suspendable suspendable, C scope) {
        try {
            Field contextField = suspendable.getClass().getDeclaredField(SCOPE);
            contextField.setAccessible(true);

            if (contextField.get(suspendable) != null)
                throw new IllegalArgumentException("Continuation already created");

            contextField.set(suspendable, scope);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return getContinuation(suspendable);
    }

    public static  Continuation getContinuation(Suspendable suspendable) {
        if (getScope(suspendable) == null)
            throw new RuntimeException("No continuation created for provided suspendable");
        //noinspection unchecked
        return ((Continuation) suspendable);
    }

    private static Scope getScope(Suspendable suspendable) {
        try {
            Field contextField = suspendable.getClass().getDeclaredField(SCOPE);
            contextField.setAccessible(true);
            return (Scope) contextField.get(suspendable);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

How does this magic work? Parameter scope through reflection the field is initialized scope$S (a synthetic field that we will create already in bytecode). A continuation is initialized once per createContinuation, and attempting to initialize again will result in execution. Then the usual type conversion to Continuation. In general, I deceived you, all the magic is not here. If such a type conversion is possible, then the specific passed Suspendable already implements Continuation. And this happened during compilation.

Project structure

The project will consist of three parts:

  • Library Code (Low Level and High Level API)
  • Tests (In fact, only in them now you can use this library)
  • Converter Suspendable -> Continuation (Implemented as gradle task in gradle buildSrc)

Since the converter is currently in buildSrc, it will be impossible to use it somewhere outside the library itself. But for now, we don’t need it. In the future, we will have two options: put it in a separate grad plugin, or make our own java agent (as it does Quasar) and perform transformations in runtime.

build.gradle

plugins {
    id "java"
}

group 'microutines'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

task processYield(type: microutine.ProcessSuspendableTask) {
    classPath = compileJava.outputs.files + compileJava.classpath
    inputs.files(compileJava.outputs.files)
}

task processTestYield(type: microutine.ProcessSuspendableTask) {
    classPath = compileJava.outputs.files + compileTestJava.classpath
    inputs.files(compileTestJava.outputs.files)
}

compileJava.finalizedBy(processYield) // после компиляции запускаем байткод модификации
compileTestJava.finalizedBy(processTestYield)

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile group: 'junit', name: 'junit', version: '4.12'
}

Conversion Suspendable in Continuation will be engaged in hail task ProcessSuspendableTask. There is nothing interesting in the hail task class, it just selects the necessary classes and sends them to convert to a class SuspendableConverter. It is he who now interests us.

Bytecode Generation

To work with bytecode, we will use the OW2 ASM library. The library works on the principle of SAX parser. We create a new ClassReader, feed it a compiled class as an array of bytes, and call the method accept(ClassVisitor visitor). ClassReader will parse the bytecode and call the appropriate methods on the passed visitor (visitMethod, visitClass, visitInsn) The visitor can work in adapter mode and delegate calls to the next visitor. Usually, the last visitor is ClassWriterin which the final bytecode is generated. If the task is non-linear (we have just one), it may take several passes through the class. Another approach provided by asm is to write the class in a special ClassNode, and do the conversion already over it. The first approach is faster, but may not be suitable for solving nonlinear problems, so I used both approaches.

Conversion Suspendable in Continuation 3 classes are involved:

  • SuspendInfoCollector – analyzes the method Suspendable.run, collects information about all calls @Suspend methods and local variables used.
  • SuspendableConverter – creates the necessary fields, changes the signature and method handle Suspendable.run, To obtain Continuation.run.
  • SuspendableMethodConverter – converts the method code Suspendable.run. Adds a code for saving and restoring local variables, saving the current state in the field label and go to the desired instructions.

Let us describe some points in more detail.

Method Search run looks like this:

MethodNode method = classNode.methods.stream()
        .filter(methodNode -> methodNode.name.equals("run") && (methodNode.access & Opcodes.ACC_BRIDGE) == 0)
        .findFirst()
        .orElseThrow(() -> new RuntimeException("Unable to find method to convert"));

There are two methods expected in a convertible class run, and one of them with a bridge modifier (what is it read here). We are interested in a method without a modifier.

In the JVM bytecode, a conditional (and unconditional) transition can be performed anywhere. ASM has a special abstraction Label (label), which is the position in the bytecode. Throughout the code after each call @Suspend methods, we will place labels to which we will make a conditional jump at the beginning of the method run.


@Override
    public void visitCode() { // событие начала метода
        super.visitCode();

        Label startLabel = new Label();

        super.visitVarInsn(Opcodes.ALOAD, THIS_VAR_INDEX); // Помещаем в стек this
        super.visitFieldInsn(Opcodes.GETFIELD, myClassJvmName, "label$S$S", "I");  //Запрашиваем поле label$S$S
        super.visitVarInsn(Opcodes.ISTORE, labelVarIndex); // Сохраняем поле в локальную переменную

        super.visitVarInsn(Opcodes.ILOAD, labelVarIndex); // помещаем переменную label в стек
        super.visitIntInsn(Opcodes.BIPUSH, 0); // помещаем 0 в стек
        super.visitJumpInsn(Opcodes.IF_ICMPEQ, startLabel); // делаем условный переход к метке startLabel если два последних значения в стеке равны (label == 0)

        for (int i = 0; i < numLabels; i++) { // делаем тоже самое, но для следующих меток
            super.visitVarInsn(Opcodes.ILOAD, labelVarIndex);
            super.visitIntInsn(Opcodes.BIPUSH, i + 1);
            super.visitJumpInsn(Opcodes.IF_ICMPEQ, labels[i]);
        }

        super.visitTypeInsn(Opcodes.NEW, "microutine/core/ContinuationEndException"); // run был вызван после завершения работы континуации, выкидываем эксепшн
        super.visitInsn(Opcodes.DUP);
        super.visitMethodInsn(Opcodes.INVOKESPECIAL, "microutine/core/ContinuationEndException", "", "()V", false);
        super.visitInsn(Opcodes.ATHROW);

        super.visitLabel(startLabel); // метка, после которой начинается пользовательский код 
    }

Place marks after calls @Suspend methods.

@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
    boolean suspendPoint = Utils.isSuspendPoint(classLoader, owner, name);

    super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
    if (suspendPoint) {
        super.visitVarInsn(Opcodes.ALOAD, THIS_VAR_INDEX); // помещаем в стек this
        super.visitIntInsn(Opcodes.BIPUSH, suspensionNumber); // помещаем номер метки, в которую вернемся в следующий раз
        super.visitFieldInsn(Opcodes.PUTFIELD, myClassJvmName, "label$S$S", "I"); // сохранем ее в поле label$S$S

        saveFrame(); // сохраняем локальные переменные

        suspend(); 
        super.visitLabel(labels[suspensionNumber - 1]); // метка, в которую мы вернемся 
        restoreFrame(); // восстанавливаем локальные переменные
        suspensionNumber++;
    }
}

private void suspend() {
    super.visitFieldInsn(Opcodes.GETSTATIC, "microutine/core/Continuation", "SUSPEND", "Ljava/lang/Object;"); // помещаем в стек Continuation.SUSPEND
    super.visitInsn(Opcodes.ARETURN); // возвращаем его
}

Tests

We write a generator that gives three numbers in a row.

testIntSequence

public class YieldTest {
    @Test
    public void testIntSequence() {
        Sequence sequence = new Sequence(new SequenceSuspendable() {
            @Override
            public void run(SequenceScope scope) {
                scope.yield(10);
                scope.yield(20);
                scope.yield(30);
            }
        });

        List list = new ArrayList<>();
        for (Integer integer : sequence) {
            list.add(integer);
        }

        assertEquals(10, (int) list.get(0));
        assertEquals(20, (int) list.get(1));
        assertEquals(30, (int) list.get(2));
    }
}

The test itself does not represent anything interesting, but it is interesting enough to decompile the class file.

testIntSequence decompiled

public class YieldTest {
    public YieldTest() {
    }

    @Test
    public void testIntSequence() {
        class NamelessClass_1 extends SequenceSuspendable implements Continuation {
            private SequenceScope scope$S;

            NamelessClass_1() {
            }

            public Object run(Object var1) {
                int label = this.label$S$S;
                SequenceScope var2;
                if (label != 0) {
                    if (label != 1) {
                        if (label != 2) {
                            if (label != 3) {
                                throw new ContinuationEndException();
                            } else {
                                var2 = this.scope$S;
                                this.label$S$S = 4;
                                return null;
                            }
                        } else {
                            var2 = this.scope$S;
                            this.yield(30);
                            this.label$S$S = 3;
                            this.scope$S = var2;
                            return Continuation.SUSPEND;
                        }
                    } else {
                        var2 = this.scope$S;
                        this.yield(20);
                        this.label$S$S = 2;
                        this.scope$S = var2;
                        return Continuation.SUSPEND;
                    }
                } else {
                    var2 = this.scope$S;
                    this.yield(10);
                    this.label$S$S = 1;
                    this.scope$S = var2;
                    return Continuation.SUSPEND;
                }
            }
        }

        Sequence sequence = new Sequence(new NamelessClass_1());
        List list = new ArrayList();
        Iterator var3 = sequence.iterator();

        while(var3.hasNext()) {
            Integer integer = (Integer)var3.next();
            list.add(integer);
        }

        Assert.assertEquals(10L, (long)(Integer)list.get(0));
        Assert.assertEquals(20L, (long)(Integer)list.get(1));
        Assert.assertEquals(30L, (long)(Integer)list.get(2));
    }
}

The code is very bloated, most of the instructions are saving and restoring the stack frame (local variables). However, it does work. The given example would work perfectly without lazy generation. Let’s consider an example more difficult.

fibonacci

public class YieldTest {
    @Test
    public void fibonacci() {
        Sequence sequence = new Sequence<>(new Suspendable() {
            @Override
            public void run(SequenceScope scope) {
                scope.yield(1);
                scope.yield(1);

                int a = 1;
                int b = 1;

                while (true) {
                    b += a;
                    scope.yield(b);
                    a += b;
                    scope.yield(a);
                }
            }
        });

        //noinspection OptionalGetWithoutIsPresent
        Integer tenthFibonacci = StreamSupport.stream(sequence.spliterator(), false)
                .skip(9).findFirst().get();

        assertEquals(55, ((int) tenthFibonacci));
    }
}

The code above generates an infinite Fibonacci sequence. We compile and decompile:

fibonacci decompiled

public class YieldTest {
    public YieldTest() {
    }

     @Test
     public void fibonacci() {
         class NamelessClass_1 extends SequenceSuspendable implements Continuation {
             private SequenceScope scope$S;
             private int aa$S;
             private int ba$S;

             NamelessClass_1() {
             }

             public Object run(Object var1) {
                 int label = this.label$S$S;
                 SequenceScope var2;
                 if (label != 0) {
                     if (label != 1) {
                         int var3;
                         int var4;
                         if (label != 2) {
                             if (label == 3) {
                                 var2 = this.scope$S;
                                 var3 = this.aa$S;
                                 var4 = this.ba$S;
                                 var3 += var4;
                                 var2.yield(var3);
                                 this.label$S$S = 4;
                                 this.scope$S = var2;
                                 this.aa$S = var3;
                                 this.ba$S = var4;
                                 return Continuation.SUSPEND;
                             }

                             if (label != 4) {
                                 throw new ContinuationEndException();
                             }

                             var2 = this.scope$S;
                             var3 = this.aa$S;
                             var4 = this.ba$S;
                         } else {
                             var2 = this.scope$S;
                             var3 = 1;
                             var4 = 1;
                         }

                         var4 += var3;
                         var2.yield(var4);
                         this.label$S$S = 3;
                         this.scope$S = var2;
                         this.aa$S = var3;
                         this.ba$S = var4;
                         return Continuation.SUSPEND;
                     } else {
                         var2 = this.scope$S;
                         var2.yield(1);
                         this.label$S$S = 2;
                         this.scope$S = var2;
                         return Continuation.SUSPEND;
                     }
                 } else {
                     var2 = this.scope$S;
                     var2.yield(1);
                     this.label$S$S = 1;
                     this.scope$S = var2;
                     return Continuation.SUSPEND;
                 }
             }
         }

         Sequence sequence = new Sequence(new NamelessClass_1());
         Integer tenthFibonacci = (Integer)StreamSupport.stream(sequence.spliterator(), false).skip(9L).findFirst().get();
         Assert.assertEquals(55L, (long)tenthFibonacci);
     }
}

Understanding what makes a decompiled class difficult enough. Like last time, most instructions drive local variables there. Some of the assignments are useless, and variables are immediately frayed by other values. To solve this problem, a deeper bytecode analysis is required, which we will not do now.

One of the interesting features of the resulting code is the lack of a while loop, although it is in the source code. There is nothing surprising in this. The body of the cycle consists of two states of a finite state machine. At the end of the loop body in bytecode there should be an unconditional transition to the condition verification code, but in our case it is ‘broken’ by two return SUSPEND.

Summary

The task that I set, in my opinion, was performed very well. We have implemented the ability to write lazy generators through yield. This can be very convenient as in the example with fibonacci numbers, but the real code that we get on the output is monstrous, incomprehensible and suboptimal. I would call only non-optimality a problem, but we will not solve it (at least in the near future). One can hope that the heated JIT compiler will skip useless assignments. Besides yield I implemented yieldAll – a method that allows you to put one sequence into another, but I did not begin to analyze the principle of its work, because it is simple to disgrace and without problems is implemented on top of everything that we have already done. If everything written up to this point is clear to you, then you can easily figure it out yourself if you look at the github.

The most difficult part of the entire library – continuations – is almost completely implemented, and is actually syntactic sugar. Building generators on top of continuations is, in principle, quite simple, and not much is left to create coroutine. I hope that already at this stage, the connection between coroutine and generators is clear: both there and there are written sequential code that runs asynchronously.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *