/* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.elasticsearch.painless.node; import org.elasticsearch.painless.AnalyzerCaster; import org.elasticsearch.painless.Definition; import org.elasticsearch.painless.Definition.Method; import org.elasticsearch.painless.Definition.Type; import org.elasticsearch.painless.FunctionRef; import org.elasticsearch.painless.Globals; import org.elasticsearch.painless.Locals; import org.elasticsearch.painless.Locals.Variable; import org.elasticsearch.painless.Location; import org.elasticsearch.painless.MethodWriter; import org.elasticsearch.painless.node.SFunction.FunctionReserved; import org.objectweb.asm.Opcodes; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; import static org.elasticsearch.painless.Definition.VOID_TYPE; import static org.elasticsearch.painless.WriterConstants.CLASS_NAME; import static org.elasticsearch.painless.WriterConstants.LAMBDA_BOOTSTRAP_HANDLE; import static org.objectweb.asm.Opcodes.H_INVOKESTATIC; /** * Lambda expression node. * <p> * This can currently only be the direct argument of a call (method/constructor). * When the argument is of a known type, it uses * <a href="http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html"> * Java's lambda translation</a>. However, if its a def call, then we don't have * enough information, and have to defer this until link time. In that case a placeholder * and all captures are pushed onto the stack and folded into the signature of the parent call. * <p> * For example: * <br> * {@code def list = new ArrayList(); int capture = 0; list.sort((x,y) -> x - y + capture)} * <br> * is converted into a call (pseudocode) such as: * <br> * {@code sort(list, lambda$0, capture)} * <br> * At link time, when we know the interface type, this is decomposed with MethodHandle * combinators back into (pseudocode): * <br> * {@code sort(list, lambda$0(capture))} */ public final class ELambda extends AExpression implements ILambda { private final String name; private final FunctionReserved reserved; private final List<String> paramTypeStrs; private final List<String> paramNameStrs; private final List<AStatement> statements; // desugared synthetic method (lambda body) private SFunction desugared; // captured variables private List<Variable> captures; // static parent, static lambda private FunctionRef ref; // dynamic parent, deferred until link time private String defPointer; public ELambda(String name, FunctionReserved reserved, Location location, List<String> paramTypes, List<String> paramNames, List<AStatement> statements) { super(location); this.name = Objects.requireNonNull(name); this.reserved = Objects.requireNonNull(reserved); this.paramTypeStrs = Collections.unmodifiableList(paramTypes); this.paramNameStrs = Collections.unmodifiableList(paramNames); this.statements = Collections.unmodifiableList(statements); } @Override void extractVariables(Set<String> variables) { for (AStatement statement : statements) { statement.extractVariables(variables); } } @Override void analyze(Locals locals) { final Type returnType; final List<String> actualParamTypeStrs; Method interfaceMethod; // inspect the target first, set interface method if we know it. if (expected == null) { interfaceMethod = null; // we don't know anything: treat as def returnType = Definition.DEF_TYPE; // don't infer any types, replace any null types with def actualParamTypeStrs = new ArrayList<>(paramTypeStrs.size()); for (String type : paramTypeStrs) { if (type == null) { actualParamTypeStrs.add("def"); } else { actualParamTypeStrs.add(type); } } } else { // we know the method statically, infer return type and any unknown/def types interfaceMethod = expected.struct.getFunctionalMethod(); if (interfaceMethod == null) { throw createError(new IllegalArgumentException("Cannot pass lambda to [" + expected.name + "], not a functional interface")); } // check arity before we manipulate parameters if (interfaceMethod.arguments.size() != paramTypeStrs.size()) throw new IllegalArgumentException("Incorrect number of parameters for [" + interfaceMethod.name + "] in [" + expected.clazz + "]"); // for method invocation, its allowed to ignore the return value if (interfaceMethod.rtn == Definition.VOID_TYPE) { returnType = Definition.DEF_TYPE; } else { returnType = interfaceMethod.rtn; } // replace any null types with the actual type actualParamTypeStrs = new ArrayList<>(paramTypeStrs.size()); for (int i = 0; i < paramTypeStrs.size(); i++) { String paramType = paramTypeStrs.get(i); if (paramType == null) { actualParamTypeStrs.add(interfaceMethod.arguments.get(i).name); } else { actualParamTypeStrs.add(paramType); } } } // gather any variables used by the lambda body first. Set<String> variables = new HashSet<>(); for (AStatement statement : statements) { statement.extractVariables(variables); } // any of those variables defined in our scope need to be captured captures = new ArrayList<>(); for (String variable : variables) { if (locals.hasVariable(variable)) { captures.add(locals.getVariable(location, variable)); } } // prepend capture list to lambda's arguments List<String> paramTypes = new ArrayList<>(captures.size() + actualParamTypeStrs.size()); List<String> paramNames = new ArrayList<>(captures.size() + paramNameStrs.size()); for (Variable var : captures) { paramTypes.add(var.type.name); paramNames.add(var.name); } paramTypes.addAll(actualParamTypeStrs); paramNames.addAll(paramNameStrs); // desugar lambda body into a synthetic method desugared = new SFunction(reserved, location, returnType.name, name, paramTypes, paramNames, statements, true); desugared.generateSignature(locals.getDefinition()); desugared.analyze(Locals.newLambdaScope(locals.getProgramScope(), returnType, desugared.parameters, captures.size(), reserved.getMaxLoopCounter())); // setup method reference to synthetic method if (expected == null) { ref = null; actual = locals.getDefinition().getType("String"); defPointer = "Sthis." + name + "," + captures.size(); } else { defPointer = null; try { ref = new FunctionRef(expected, interfaceMethod, desugared.method, captures.size()); } catch (IllegalArgumentException e) { throw createError(e); } // check casts between the interface method and the delegate method are legal for (int i = 0; i < interfaceMethod.arguments.size(); ++i) { Type from = interfaceMethod.arguments.get(i); Type to = desugared.parameters.get(i + captures.size()).type; AnalyzerCaster.getLegalCast(location, from, to, false, true); } if (interfaceMethod.rtn != VOID_TYPE) { AnalyzerCaster.getLegalCast(location, desugared.rtnType, interfaceMethod.rtn, false, true); } actual = expected; } } @Override void write(MethodWriter writer, Globals globals) { writer.writeDebugInfo(location); if (ref != null) { writer.writeDebugInfo(location); // load captures for (Variable capture : captures) { writer.visitVarInsn(capture.type.type.getOpcode(Opcodes.ILOAD), capture.getSlot()); } writer.invokeDynamic( ref.interfaceMethodName, ref.factoryDescriptor, LAMBDA_BOOTSTRAP_HANDLE, ref.interfaceType, ref.delegateClassName, ref.delegateInvokeType, ref.delegateMethodName, ref.delegateType ); } else { // placeholder writer.push((String)null); // load captures for (Variable capture : captures) { writer.visitVarInsn(capture.type.type.getOpcode(Opcodes.ILOAD), capture.getSlot()); } } // add synthetic method to the queue to be written globals.addSyntheticMethod(desugared); } @Override public String getPointer() { return defPointer; } @Override public org.objectweb.asm.Type[] getCaptures() { org.objectweb.asm.Type[] types = new org.objectweb.asm.Type[captures.size()]; for (int i = 0; i < types.length; i++) { types[i] = captures.get(i).type.type; } return types; } @Override public String toString() { return multilineToString(pairwiseToString(paramTypeStrs, paramNameStrs), statements); } }