Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add @Generator annotation #3372

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

storycraft
Copy link

@storycraft storycraft commented Mar 15, 2023

This PR introduces @Generator annotation.

@Generator annotation converts normal method into generator(lazily evaluated iterator). It provides a easy way to create custom iterators. Creating iterators by hand are very hard and complex.

Using @Generator annotation, user can suspend execution and yield values to iterator using yieldThis or yieldAll statement. The method must return Iterable or Iterator.

This PR closes #560 (Same functionality)

Transformation steps

  1. The code in method annotated with @Generator is wrapped with internal local class __Generator by handler. The __Generator class extends lombok.Lombok.Generator which is stub class implementing Iterable, Iterator. The stub class also have blank yieldThis, yieldAll method declaration.
  2. Classes extending lombok.Lombok.Generator class is transformed in post compiler phase using bytecode manipulation. The transformer remove stub class, capture every local variable into field, and transform yieldThis, yieldAll statements. Efficiently transforms into state machine.

Code example

Method range yields number from from to to, and method join delegates multiple iterables by order.

With Lombok

import lombok.experimental.Generator;

public class GeneratorExample {
    @Generator
    public Iterable<Integer> range(int from, int to) {
        for (int i = from; i < to; i++) {
            yieldThis(i);
        }
    }

    @SafeVarargs
    @Generator
    public final <T> Iterable<T> join(Iterable<T>... iters) {
        for (Iterable<T> iter : iters) {
            yieldAll(iter);
        }
    }
}

Generated code by Lombok handler

public class GeneratorExample {
    public Iterable<Integer> range(int from, int to) {
        class __Generator extends lombok.Lombok.Generator<Integer> {
            protected void advance() {
                for (int i = from; i < to; i++) {
                    yieldThis(i);
                }
            }
        }
        return new __Generator();
    }

    @SafeVarargs
    public final <T> Iterable<T> join(Iterable<T>... iters) {
        class __Generator extends lombok.Lombok.Generator<T> {
            protected void advance() {
                for (Iterable<T> iter : iters) {
                    yieldAll(iter);
                }
            }
        }
        return new __Generator();
    }
}

After bytecode manipulation, cannot be decompiled into java code.

@kevin-scholten
Copy link

I think you meant to swap the 2 snippets of code? The PR description now shows the use of the annotation under 'before lombok'. Minor detail 😄

@storycraft
Copy link
Author

storycraft commented Mar 16, 2023

I think you meant to swap the 2 snippets of code? The PR description now shows the use of the annotation under 'before lombok'. Minor detail 😄

It was to show how handler transform annotated code. I updated description. It will be less confusing now.

On javac handler, body statements are not marked as generated anymore.
Error if yields inside local class, lambda.
Introduce additional boolean field to check if generator peeked.
@storycraft
Copy link
Author

Can someone review this pr?

@janmaterne
Copy link
Contributor

Besides I got the usecase, I dislike introducing a runtime dependency on Lombok classes.

@storycraft
Copy link
Author

storycraft commented Jan 28, 2024

Besides I got the usecase, I dislike introducing a runtime dependency on Lombok classes.

The lombok.experimental.Generator class only required in compile time to simplify bytecode modification process. The modification process turns the class into complete state machine so there is no runtime dependency in final built bytecode!

@Rawi01
Copy link
Collaborator

Rawi01 commented Jan 28, 2024

@storycraft Can you explain to me why we need this feature in lombok? Both examples can be easily coded using the Stream API or common librarys. At the moment, I see little benefit and a high maintenance burden, which would disqualify this feature.

The second showstopper is the bytecode transformation. That strategy makes it impossible to delombok the code and write the tests in the usual format. Thats probably why there are no tests for the generate state machine 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

"Yield" feature pull request
4 participants