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

Fix Painless Lambdas for Java 9 #24070

Merged
merged 19 commits into from
Apr 24, 2017
Merged

Fix Painless Lambdas for Java 9 #24070

merged 19 commits into from
Apr 24, 2017

Conversation

jdconrad
Copy link
Contributor

JDK9 has made changes to LambdaMetaFactory that prevent Painless lambdas from being able to do proper adaptation of types from the interface method to the delegate method even using a bridge. In particular boxed types are no longer widened appropriately, and Object can no longer be adapted to primitive types. Though, the former is likely a bug based on the spec, whether the latter is allowed according to spec is somewhat ambiguous.

This fix adds a LambdaBootstrap class that replaces LambdaMetaFactory in Painless using the same methodology that LambdaMetaFactory was based on (http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html). This allows us to do whatever adaptations we believe are necessary without having to worry about future changes/bugs in LambdaMetaFactory.

This is related to #23473.

@@ -63,7 +63,7 @@
}

/**
* A secure class loader used to define Painless scripts.
* A secure class loader used to defineScript Painless scripts.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental refactor.

}

/**
* Runs the two-pass compiler to generate a Painless script.
* @param <T> the type of the script
* @param loader The ClassLoader used to define the script.
* @param loader The ClassLoader used to defineScript the script.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental refactor.

java.lang.reflect.Constructor<? extends PainlessScript> constructor =
clazz.getConstructor(String.class, String.class, BitSet.class);

return iface.cast(constructor.newInstance(name, source, root.getStatements()));
} catch (Exception exception) { // Catch everything to let the user know this is something caused internally.
throw new IllegalStateException("An internal error occurred attempting to define the script [" + name + "].", exception);
throw new IllegalStateException("An internal error occurred attempting to defineScript the script [" + name + "].", exception);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leftover?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental refactor.

* <p>
* Once you have created one of these, you have "everything you need" to call LambdaMetaFactory
* either statically from bytecode with invokedynamic, or at runtime from Java.
* Once you have created one of these, you have "everything you need" to call LambdaBootstrap
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{@link LambdaBootstrap}? The advantage of the @link is that IDEs will automatically fix the comment if you rename the class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

* lambda functions and method references within Painless. The code generation
* used here is based upon the following article:
* http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
* However, it is a simplified version as Painless has no concept of generics.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nor serialization which the link goes into a lot of depth about.

Also, one of the key reasons that they took that strategy for java code is that the compiler doesn't know what runtime it is going to be run under. invokedynamic future proofs the generated code. We don't have that concern. So I expect we can make different choices there too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chatted with @jdconrad - we have a different reason for needing invokedynamic than java does - we don't know the target functional interface until runtime sometimes. If you invoke a method on a def object that takes a lambda then we must delay building the class. For simplicity, we always delay.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about serialization.

* this.arg$0 = arg$0;
* }
*
* public static $$Lambda0 get$Lambda(List arg$0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading this from top to bottom, I'm curious why we need this instead of calling the ctor directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To invoke a constructor the class would have to be newed and duped onto the stack. The caller doesn't know what the generated class is, so it calls a factorymethod that can create the class instead.


/**
* A counter used to generate a unique name
* for each lambda function/reference class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably worth leaving a comment that this is done in the child classloader so this is a per-script counter.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like that's not the case as when I test with two independent scripts the counter is incremented both times rather than starting at 1 for both of them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is only a single LambdaBootstrap so there is only one counter. I'd prefer one per script so I could look at the counter and know how many lambdas we've generated but I don't think that is worth holding up the PR for.

Copy link
Contributor Author

@jdconrad jdconrad Apr 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not worth complicating this to have to reload this class on a per-script basis. If you really want know the lambda count during debugging you can run one script at a time in a debugger. Plus, you also already know the answer since you generate exactly one lambda factory method per lambda per target type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, it isn't worth holding the PR for. I'd like it for debugging issues in running systems rather than the debugger. But it isn't a big deal.

* @throws LambdaConversionException Thrown when an illegal type conversion occurs at link time
*/
public static CallSite lambdaBootstrap(
Lookup lookup,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you indent this one extra time so it doesn't look like method body?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@jdconrad
Copy link
Contributor Author

@nik9000 Ready for another round -- think I responded to all comments.

Copy link
Member

@nik9000 nik9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't say I really understand all the subtleties but I think this makes sense.

I think it'd be worth testing what happens when you blow PIC and this falls back to megamorphic. I'm not sure if this code is worse or better than LambdaMetaFactory in that case but I'd love to understand it.

return defineClass(name, bytes, 0, bytes.length, CODESOURCE).asSubclass(PainlessScript.class);
}

Class<?> defineLambda(String name, byte[] bytes) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about javadocs for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

owner = WriterConstants.AUGMENTATION_TYPE.getInternalName();

if ("<init>".equals(delegateMethod.name)) {
delegateInvokeType = Opcodes.H_NEWINVOKESPECIAL;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd either statically import all of these opcodes or none of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Fixed.

@@ -33,7 +33,7 @@
/**
* Represents a method call.
*/
final class PSubCallInvoke extends AExpression {
final class PSubCallInvoke extends AExpression {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

* {@code
* List list1 = new ArrayList(); "
* list1.add(2); "
* List1 list2 = new ArrayList(); "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/List1/List/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


/**
* A counter used to generate a unique name
* for each lambda function/reference class.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, there is only a single LambdaBootstrap so there is only one counter. I'd prefer one per script so I could look at the counter and know how many lambdas we've generated but I don't think that is worth holding up the PR for.

* of either a lot more code or requiring many {@link Definition.Type}s to be looked
* up at link-time.
*/
public static CallSite delegateBootstrap(Lookup lookup,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this method up with the other public method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to leave this one as the methods are in order of which ones will be called first.

@jdconrad
Copy link
Contributor Author

jdconrad commented Apr 18, 2017

I don't understand this -- "I think it'd be worth testing what happens when you blow PIC and this falls back to megamorphic. I'm not sure if this code is worse or better than LambdaMetaFactory in that case but I'd love to understand it." Can you please explain what you mean here? Lambdas don't use this PIC at all except indirectly through another def type. A direct lambda cannot be the target type ever. For that def type to even fall back to megamorphic requires the target type to change I believe at least 6 times. (I'm not even sure this is possible using lambdas given our current supported whitelist.) If a user has a def value representing a lambda it's simply thought of as the functional interface type. I also know the fallback is tested for other types already. Maybe it's interesting in an academic sense, but it's really not practically necessary at this time.

@nik9000
Copy link
Member

nik9000 commented Apr 18, 2017

Maybe it's interesting in an academic sense, but it's really not practically necessary at this time.

Sure. I'm debugging some issues someone pinged me about where our stack traces aren't good when we fall back to the megamorphic case so I'm probably overthinking this.

Copy link
Member

@nik9000 nik9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's get this in so we aren't busted in Java 9.

@jdconrad
Copy link
Contributor Author

@nik9000 Is there an issue up for the stack traces? I'm curious to see what the difference in stack traces looks like.

Also, thanks for the review! I will make sure to respond to the rest of the comments before merging.

@nik9000
Copy link
Member

nik9000 commented Apr 18, 2017

Is there an issue up for the stack traces? I'm curious to see what the difference in stack traces looks like.

I'm playing with it now. Someone pinged my privately. I'm still trying to reproduce.

@nik9000
Copy link
Member

nik9000 commented Apr 18, 2017

So yes, there is an issue with stack traces but it isn't unexpected. Basically the PIC and MIC end up in stacktraces when def is involved and that makes things hard to read. But you should be using script_stack to debug issues like these anyway.

@jdconrad jdconrad merged commit 30cc33e into elastic:master Apr 24, 2017
jdconrad added a commit that referenced this pull request Apr 24, 2017
Replaces LambdaMetaFactory with LambdaBootstrap, a custom solution for lambdas in Painless using a design similar to LambdaMetaFactory, but allows for custom adaptation of types which recent changes to LambdaMetaFactory no longer allowed.
@jdconrad
Copy link
Contributor Author

@nik9000 Thank you for the review.

@nik9000
Copy link
Member

nik9000 commented Apr 24, 2017

Thanks for diving into LambdaMetaFactory!

@uschindler
Copy link
Contributor

uschindler commented Apr 29, 2017

I figured out that the special handling of H_NEWINVOKESPECIAL is broken. The following test fails:

    public void testCtorWithParams() {
        assertArrayEquals(new Object[] { "foo", "bar" },
                (Object[]) exec("List l = new ArrayList(); l.add('foo'); l.add('bar'); " +
                        "Stream stream = l.stream().map(StringBuilder::new);" +
                        "return stream.map(Object::toString).toArray()"));
    }

This code fails because the script returns a Object[] consisting only of 2 empty Strings. The reason for this is: StringBuilder::new points to the StringBuilder(CharSequence) ctor. The code in LambdaBootstrap ignores all parameters and calls the default ctor of StringBuilder. The result is mapped back to a String afterwards and is of course empty.

I have a fix ready...

@uschindler
Copy link
Contributor

uschindler commented Apr 29, 2017

In addition, the static factory as replacement for the constructor of the lambda (get$lambda) is not needed. If you use lookup.findConstructor() everything works as it should. I will post both fixes in a PR.

@uschindler
Copy link
Contributor

I opened PR #24406 to fix both issues. All tests pass for me with Java 8 and Java 9.

@uschindler
Copy link
Contributor

uschindler commented Apr 29, 2017

I have seen some discussions initiated by @nik9000 and @jdconrad about the useless stack frames we see in the stack trace on exceptions. Since Java 8u60, the whole Lambda class is hidden from all stack traces (so user only sees a direct call in the stack trace to the lambda/function reference, but no wrapper classes).

The trick is to mark methods that you want to hide (because they are completely synthetic and have no useful meaning to users reading the stack). This would also solve the problem with the ever-incrementing class name counter.

This is the change in Java 8u60: http://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/rev/f7dd864a52ea

We can do the same (after #24406 is merged):

iface.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);

The problem is that this runtime annotation is internal only (OpenJDK specific). So I'd only add the annotation to the interface method and the ctor proxy only if Class.forName("java.lang.invoke.LambdaForm$Hidden") works. If you agree, I will provide a PR soon after #24406 is merged.

@uschindler
Copy link
Contributor

I tried to hide the stack frames like done above. Unfortunately it does not seem to work if the classes are not anonymous (via Unsafe).

During that I found some smaller things, like the doPrivileged around getting the ctor: This is not needed, as it is public in our case. I can provide a PR later. I'd also change LambdaConversionException like the original LambdaMetaFactory on problems instead of IllegalStateException (which is wrong).

@uschindler
Copy link
Contributor

uschindler commented Apr 30, 2017

As hiding the stack frames did not work, I made the counter "per-script". The trick was to put an AtomicInteger into the Compiler.Loader instance, which is accessible from LambdaBootstrap. As it is no longer global, an integer is fine. I also removed the doPrivileged for the non-capturing instance. See b547fa9

I don't like the IllegalStateExceptions in LambdaBootstrap instead of LambdaConversionExceptions. We should also change that. OpenJDKs bootstrap throws LambdaConversionException on every problem while creating the lambda class.

@uschindler
Copy link
Contributor

Finally I also changed the codesource parameter in defineClass to be consistent in my PR. The lambdas are now running in same unprivileged code source.

Copy link
Contributor

@uschindler uschindler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I solved the mentioned issues in my other PR #24406

// A new instance of the requested type will be created and the
// constructor with no parameters will be called.
// Example: String::new
if (delegateInvokeType == H_NEWINVOKESPECIAL) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure here that we never have arguments for ctor references? If no (e.g., painless does not support ::new with args) we should assert this and throw exception!

* @return A Class object.
*/
Class<?> defineLambda(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not here also use CODESOURCE for safety? I know the lambda class on its own cannot do any bad stuff, but I'd still make it use the same codesource!

@jdconrad jdconrad deleted the lambda branch May 11, 2017 16:27
@clintongormley clintongormley added :Core/Infra/Scripting Scripting abstractions, Painless, and Mustache and removed :Plugin Lang Painless labels Feb 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants