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

Same-Class Lambdas #187

Open
wants to merge 18 commits into
base: feature/refactor
Choose a base branch
from

Conversation

TheSilkMiner
Copy link
Member

@TheSilkMiner TheSilkMiner commented Feb 27, 2025

Warning

This PR depends on #186 and ZenCodeLang/StdLibs#13. Those PRs must be merged first to ensure proper functionality.

In a Nutshell

Finally this works!

public class Foo {
  foo(): void { lambda(() => bar()); }
  private bar(): void {}
  private lambda(block: function() as void): void { block(); }
}

Description

This PR brings a big set of changes with respect to how lambdas are compiled and, as sort of the groundwork for that, expands the capabilities of the compiler with both a new module and the ability to leverage Java 7's invokedynamic instruction (and potentially ConstantDynamic? Though I haven't tested that as we don't need it yet). The full list of changes will be described in the paragraphs that follow.

JavaRuntime

JavaRuntime is a new module that has been added and, as the name suggests, it aims to be the runtime counterpart to JavaBytecodeCompiler and in general of the Java compiler for the ZenCode language. Its goal is to provide a place where all components of the language that need to exist at runtime to ensure proper execution of scripts can be placed. Examples include reflection utilities, a potential ZenType in the future to ensure proper reification with Java integration, and invokedynamic utilities.

In general, the idea is to assume that precompiled programs (or scripts) could be executed on the JVM without any other module but JavaRuntime.

invokedynamic support in JavaWriter

JavaWriter has been expanded to provide a set of utilities that allow processing invokedynamic instructions and ConstantDynamic types1. Essentially, JavaWriter#invokeDynamic can now be used to write an invokedynamic instruction into bytecode, while JavaWriter#constant (and JavaWriter#ldc) support dynamic constants via JavaCondy.

All the details representing the call site2 and the BSM3 are represented by classes in the indy package, respectively JavaIndy/JavaCondy and BsmData. They are all constructed via builders and provide immediate validation.

invokedynamic-based same-class lambdas

The major change introduced by this PR: lambdas are now compiled as methods located within the same class that defines them and the corresponding functional interface instance is dynamically generated at runtime through invokedynamic and LambdaFactory. This brings various advantages:

  1. Lambdas now have access to all public, private, protected, and default-visibility members of the class they are defined, without incurring into the all-too-common IllegalAccessError that plagued the language previously4.
  2. invokedynamic means that the generation of classes implementing lambdas is done at runtime with an algorithm that can be evolved separately from the compiler; in turn this means that if we were to find alternatives to class generation, potentially precompiled programs would benefit from it without requiring recompilation.
  3. invokedynamic ensures that lambdas are only generated when needed, so if a particular code path involving a lambda is never invoked, the corresponding class won't exist, saving up space.
  4. Not only less clutter if generated classes are exported, but now all methods pertaining to a class are located within the same class, making for easier debugging.

Unfortunately, this also brings a disadvantage. Namely FernFlower isn't really able to understand the invokedynamic method, and thus it replaces it with a very... peculiar instruction. Nevertheless, the general code flow is still understandable, so I consider this a minor issue.

Minor changes

This PR also brings some more minor improvements, which do not warrant a separate section and are thus described here.

  • Building upon invokedynamic lambdas, converting between functional interfaces now also goes through LambdaFactory, cleverly reusing it and thus reducing code duplication.
  • Shared runs have been added, allowing team members to always have a test run available and properly configured in order to run tests5.
  • Mangling of lambdas now carries more information, including the method name in which they are defined and the type of the class that is being implemented.
  • JavaClass can now be constructed via a Class file, simplifying usage with Java classes.
  • Variables in lambdas are now properly named according to the function parameters, if available; the same applies to captures, which get prefixed with $capture$ to avoid conflicts, and the receiver ($this).

Footnotes

  1. ConstantDynamic is not supported in Java 8: it was added around Java 11. So technically all code supporting it is useless as of now, but I figured we might as well have the framework for it ready to leverage it in the future if we decide to either migrate to newer Java versions or have Java-version-dependent compilation.

  2. The call site is essentially the pair representing the method name and the descriptor of the method that will be dynamically invoked by the invokedynamic instruction.

  3. A bootstrap method (or BSM for short) represents a method that is invoked by the JVM when an invokedynamic or ConstantDynamic is found, allowing for the construction of the dynamic method call or constant respectively.

  4. Unfortunately, due to limitations in the APIs used due to Java 8, the class needs to be declared public regardless, otherwise an IllegalAccessError occurs. This could be worked around, but the API for it is not only internal to the JDK (Java 15 provides Hidden Classes as API, which is what we want), but also has been removed in newer Java versions.

  5. Speaking of, if someone can fix the IntelliJ based run, that'd be a great help! 😅

jaredlll08 and others added 18 commits February 8, 2025 19:45
Use version catalogues to keep versions in sync
Update to gradle 8.8
This ensures Fernflower doesn't complain about invalid method names.
This change might be reverted in the future.

Signed-off-by: TheSilkMiner <[email protected]>
Signed-off-by: TheSilkMiner <[email protected]>
This module will house all runtime-executing components for the Java
integration of the language. In other words, if any compiler class needs
to be present at runtime for scripts or other code to be executed (NOT
compiled), then it will be located into this module.

Examples include reflection utilities, custom classes that might be used
by Java reflection to inspect scripts, INDY factories etc.

Essentially, assuming a script has already been compiled to bytecode,
this module should contain the code needed to allow the script to run
(excluding any additional environment-provided classes).

Signed-off-by: TheSilkMiner <[email protected]>
- Compile them as just another private method in the same class
  This means lambdas cannot be invoked (if not through reflection) and
  they have access to all public, default, private, protected members
  without any access restrictions.
- Use INDY to generate the actual lambda implementation class at runtime
  This allows us to change the algorithm at any point without having to
  touch the compiler, allowing for an easier language evolution over
  time. Moreover, it also allows us to put the various classes in the
  correct package and, potentially, as nest-mates of the owner,
  bypassing all access restrictions. Furthermore, it gives us
  flexibility for compatibility with newer Java versions.

Signed-off-by: TheSilkMiner <[email protected]>
Signed-off-by: TheSilkMiner <[email protected]>
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.

2 participants