Skip to content

Commit

Permalink
Extract a helper class for constructing lambdas from TemplateCompiler
Browse files Browse the repository at this point in the history
This will soon be reused in a few different places.

Add a tiny optimization for lambdas with zero bound parameters by compiling them to an `ldc` instruction.  This special case happens to satisfy all of our current usecases and will satisfy a subset of the upcoming ones.  This should be a tiny bootstraping and codesize improvement.

PiperOrigin-RevId: 575868161
  • Loading branch information
lukesandberg authored and copybara-github committed Oct 23, 2023
1 parent 028ebe5 commit df99123
Show file tree
Hide file tree
Showing 5 changed files with 303 additions and 28 deletions.
30 changes: 2 additions & 28 deletions java/src/com/google/template/soy/jbcsrc/TemplateCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.google.template.soy.jbcsrc.restricted.CodeBuilder;
import com.google.template.soy.jbcsrc.restricted.Expression;
import com.google.template.soy.jbcsrc.restricted.FieldRef;
import com.google.template.soy.jbcsrc.restricted.LambdaFactory;
import com.google.template.soy.jbcsrc.restricted.LocalVariable;
import com.google.template.soy.jbcsrc.restricted.MethodRef;
import com.google.template.soy.jbcsrc.restricted.MethodRefs;
Expand All @@ -66,7 +67,6 @@
import com.google.template.soy.types.NullType;
import com.google.template.soy.types.TemplateType;
import com.google.template.soy.types.TemplateType.Parameter;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
Expand Down Expand Up @@ -153,18 +153,6 @@ void compile() {
generateModifiableSelectMethod();
}

private static final Handle METAFACTORY_HANDLE =
MethodRef.createPure(
LambdaMetafactory.class,
"metafactory",
MethodHandles.Lookup.class,
String.class,
MethodType.class,
MethodType.class,
MethodHandle.class,
MethodType.class)
.asHandle();

private static final Handle DELEGATE_FACTORY_HANDLE =
MethodRef.createPure(
RecordToPositionalCallFactory.class,
Expand All @@ -176,9 +164,6 @@ void compile() {
String[].class)
.asHandle();

private static final String COMPILED_TEMPLATE_INIT_DESCRIPTOR =
Type.getMethodDescriptor(BytecodeUtils.COMPILED_TEMPLATE_TYPE);

private static final Type COMPILED_TEMPLATE_RENDER_DESCRIPTOR =
Type.getMethodType(
BytecodeUtils.RENDER_RESULT_TYPE,
Expand All @@ -203,18 +188,7 @@ private void generateTemplateMethod(MethodRef templateMethod, MethodRef renderMe
// assuming foo is the name of the template class.
Statement methodBody =
Statement.returnExpression(
new Expression(BytecodeUtils.COMPILED_TEMPLATE_TYPE) {
@Override
protected void doGen(CodeBuilder cb) {
cb.visitInvokeDynamicInsn(
"render",
COMPILED_TEMPLATE_INIT_DESCRIPTOR,
METAFACTORY_HANDLE,
COMPILED_TEMPLATE_RENDER_DESCRIPTOR,
renderMethod.asHandle(),
COMPILED_TEMPLATE_RENDER_DESCRIPTOR);
}
});
LambdaFactory.create(MethodRefs.COMPILED_TEMPLATE_RENDER, renderMethod).invoke());
CodeBuilder methodWriter =
new CodeBuilder(methodAccess(), templateMethod.method(), /* exceptions= */ null, writer);
generateTemplateMetadata(methodWriter);
Expand Down
162 changes: 162 additions & 0 deletions java/src/com/google/template/soy/jbcsrc/restricted/LambdaFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2023 Google Inc.
*
* Licensed 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 com.google.template.soy.jbcsrc.restricted;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static java.util.Arrays.asList;

import com.google.common.collect.ImmutableList;
import com.google.template.soy.jbcsrc.restricted.Expression.Feature;
import com.google.template.soy.jbcsrc.restricted.Expression.Features;
import com.google.template.soy.jbcsrc.shared.ExtraConstantBootstraps;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import org.objectweb.asm.ConstantDynamic;
import org.objectweb.asm.Handle;
import org.objectweb.asm.Type;

/**
* A helper for generating lambda callsites. Allows for some extra compile time checking and
* encapsulation.
*/
public final class LambdaFactory {

private static final Handle METAFACTORY_HANDLE =
MethodRef.createPure(
LambdaMetafactory.class,
"metafactory",
MethodHandles.Lookup.class,
String.class,
MethodType.class,
MethodType.class,
MethodHandle.class,
MethodType.class)
.asHandle();
private static final Handle CONDY_METAFACTORY =
MethodRef.createPure(
ExtraConstantBootstraps.class,
"constantMetafactory",
MethodHandles.Lookup.class,
String.class,
Class.class,
MethodType.class,
MethodHandle.class,
MethodType.class)
.asHandle();

/**
* Create a {@code LambdaFactory} that can create instances of an interface that implements {@code
* interfaceMethod} by delegating to {@code implMethod}.
*/
public static LambdaFactory create(MethodRef interfaceMethod, MethodRef implMethod) {
checkState(interfaceMethod.isInterfaceMethod(), "Only interfaces are supported");
return new LambdaFactory(interfaceMethod.owner(), interfaceMethod, implMethod);
}

private final TypeInfo interfaceType;
private final String interfaceMethodName;
private final Type interfaceMethodType;
private final MethodRef implMethod;
private final String callSiteDescriptor;
private final ImmutableList<Type> boundParams;

private LambdaFactory(TypeInfo interfaceType, MethodRef interfaceMethod, MethodRef implMethod) {
this.interfaceType = interfaceType;
this.interfaceMethodName = interfaceMethod.method().getName();
this.implMethod = implMethod;
// Skip the first param as it is the receiver type. MethodRef models 'receivers' as an argument
// but the jdk models them separately so from the perspective of the lambda-metafactory it isn't
// part of the interface method signature.
var interfaceMethodParams =
interfaceMethod.argTypes().subList(1, interfaceMethod.argTypes().size());
this.interfaceMethodType =
Type.getMethodType(
interfaceMethod.returnType(), interfaceMethodParams.toArray(new Type[0]));
// Lambdas are essentially an inner classes
// class Impl extends Interface {
// Impl(...boundParams) {...}
// method(..freeParams) { return target(..boundParams, ...freeParams);}
// }
// So all 'free' params must be in trailing argument position of the target method and exactly
// match the interface method parameter types.
// The JDK does allow for there to be some mismatch, as long as the mismatches are resolvable
// with simple type coercion rules (aka if the java cast operator could handle it, so could the
// lambda factory). However, for now we require an exact match.
var trailingImplParams =
implMethod
.argTypes()
.subList(
implMethod.argTypes().size() - interfaceMethodParams.size(),
implMethod.argTypes().size());
checkArgument(
interfaceMethodParams.equals(trailingImplParams),
"trailing parameters of %s must match the free parameters of %s",
implMethod,
interfaceMethod);
this.boundParams =
implMethod
.argTypes()
.subList(0, implMethod.argTypes().size() - interfaceMethodParams.size());
this.callSiteDescriptor =
Type.getMethodDescriptor(interfaceType.type(), boundParams.toArray(new Type[0]));
}

public Expression invoke(Expression... args) {
return invoke(asList(args));
}

public Expression invoke(Iterable<Expression> args) {
Expression.checkTypes(boundParams, args);
var features = Features.of(Feature.NON_JAVA_NULLABLE, Feature.NON_SOY_NULLISH);
if (Expression.areAllCheap(args)) {
features = features.plus(Feature.CHEAP);
}
return new Expression(interfaceType.type(), features) {
@Override
protected void doGen(CodeBuilder cb) {
// When there are no bound parameters we can link this with condy instead of indy.
// According to Brian Goetz this should be faster to link even if the impls are identical
// See https://mail.openjdk.org/pipermail/amber-dev/2023-October/008327.html
if (boundParams.isEmpty()) {
cb.visitLdcInsn(
new ConstantDynamic(
interfaceMethodName,
interfaceType.type().getDescriptor(),
CONDY_METAFACTORY,
interfaceMethodType,
implMethod.asHandle(),
interfaceMethodType));

} else {
for (Expression arg : args) {
arg.gen(cb);
}
cb.visitInvokeDynamicInsn(
interfaceMethodName,
callSiteDescriptor,
METAFACTORY_HANDLE,
interfaceMethodType,
implMethod.asHandle(),
interfaceMethodType);
}
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ public Handle asHandle() {
owner().isInterface());
}

boolean isInterfaceMethod() {
return owner().isInterface() && opcode() == Opcodes.INVOKEINTERFACE;
}

// TODO(lukes): consider different names. 'invocation'? invoke() makes it sounds like we are
// actually calling the method rather than generating an expression that will output code that
// will invoke the method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.google.template.soy.jbcsrc.shared;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.invoke.MethodType.methodType;
import static java.util.Arrays.stream;

import com.google.common.collect.ImmutableList;
Expand All @@ -27,7 +28,10 @@
import com.google.template.soy.data.internal.ParamStore;
import com.google.template.soy.data.internal.SoyMapImpl;
import com.google.template.soy.data.internal.SoyRecordImpl;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/** Extra constant bootstrap methods. */
public final class ExtraConstantBootstraps {
Expand Down Expand Up @@ -100,5 +104,27 @@ public static RecordProperty symbol(MethodHandles.Lookup lookup, String name, Cl
return RecordProperty.get(name);
}

// JDK has half implemented support for invoking lambdas via condy.
// There is a special case for this exact signature
// See BootstrapMethodInvoker+ isLambdaMetafactoryCondyBSM
// and https://bugs.openjdk.org/browse/JDK-8198418
// According to Brian Goetz this got de-staffed for priority and scope creep reasons but is still
// a good idea because condy linkage is cheaper than invokedynamic
// https://mail.openjdk.org/pipermail/amber-dev/2023-October/008327.html
@Keep
public static Object constantMetafactory(
MethodHandles.Lookup lookup,
String name,
Class<?> type,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws Throwable {
return LambdaMetafactory.metafactory(
lookup, name, methodType(type), samMethodType, implMethod, instantiatedMethodType)
.getTarget()
.invoke();
}

private ExtraConstantBootstraps() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2023 Google Inc.
*
* Licensed 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 com.google.template.soy.jbcsrc.restricted;

import static com.google.common.truth.Truth.assertThat;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.constant;

import com.google.template.soy.jbcsrc.restricted.testing.ExpressionEvaluator;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class LambdaFactoryTest {
// Interface methods to implement with a lambda
private static final MethodRef SUPPLIER_GET = MethodRef.createPure(Supplier.class, "get");
private static final MethodRef FUNCTION_APPLY =
MethodRef.createPure(Function.class, "apply", Object.class);
private static final MethodRef BIFUNCTION_APPLY =
MethodRef.createPure(BiFunction.class, "apply", Object.class, Object.class);

// Implementation referecences
private static final MethodRef IDENTITY_FUNCTION =
MethodRef.createPure(LambdaFactoryTest.class, "identity", Object.class);
private static final MethodRef RETURNS_HELLO_FUNCTION =
MethodRef.createPure(LambdaFactoryTest.class, "returnsHello");
private static final MethodRef CONCAT_FUNCTION =
MethodRef.createPure(LambdaFactoryTest.class, "concat", Object.class, Object.class);

// Actual implementations
public static Object identity(Object o) {
return o;
}

public static Object concat(Object o, Object o2) {
return o + ", " + o2;
}

public static Object returnsHello() {
return "hello";
}

@Test
public void testNoBoundParameters() throws ReflectiveOperationException {
@SuppressWarnings("unchecked")
Supplier<Object> supplier =
(Supplier)
ExpressionEvaluator.evaluate(
LambdaFactory.create(SUPPLIER_GET, RETURNS_HELLO_FUNCTION).invoke());

assertThat(supplier.get()).isEqualTo("hello");
}

@Test
public void testOneBundParameter() throws ReflectiveOperationException {
@SuppressWarnings("unchecked")
Supplier<Object> supplier =
(Supplier)
ExpressionEvaluator.evaluate(
LambdaFactory.create(SUPPLIER_GET, IDENTITY_FUNCTION).invoke(constant("bound")));
assertThat(supplier.get()).isEqualTo("bound");
}

@Test
public void testOneFreeParameter() throws ReflectiveOperationException {
@SuppressWarnings("unchecked")
Function<Object, Object> fn =
(Function)
ExpressionEvaluator.evaluate(
LambdaFactory.create(FUNCTION_APPLY, IDENTITY_FUNCTION).invoke());
assertThat(fn.apply("free")).isEqualTo("free");
}

@Test
public void testOneFreeAndOneBoundParameter() throws ReflectiveOperationException {
@SuppressWarnings("unchecked")
Function<Object, Object> fn =
(Function)
ExpressionEvaluator.evaluate(
LambdaFactory.create(FUNCTION_APPLY, CONCAT_FUNCTION).invoke(constant("first")));
assertThat(fn.apply("second")).isEqualTo("first, second");
}

@Test
public void testTwoFreeParameters() throws ReflectiveOperationException {
@SuppressWarnings("unchecked")
BiFunction<Object, Object, Object> fn =
(BiFunction)
ExpressionEvaluator.evaluate(
LambdaFactory.create(BIFUNCTION_APPLY, CONCAT_FUNCTION).invoke());
assertThat(fn.apply("first", "second")).isEqualTo("first, second");
}
}

0 comments on commit df99123

Please sign in to comment.