Skip to content

Make possible SoySourceFunction's with zero overhead in JBCSRC runtime, compared with a BuiltinFunction. SoyJavaExternFunction can return a Method appropriate for runtime type of the arguments(s), to avoid unnecessary boxing/unboxing. SoyJavaExternFunction can replace SoyJavaSourceFunction. Convert a few built-ins to SoyJavaExternFunction where codegenning based on whether the values are boxed is advantageous. #2404

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

Merged
merged 1 commit into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ public boolean isAssignableFrom(TypeReference other) {
return true;
}

public boolean isPrimitive() {
String className = className();
return className.indexOf('.') < 0 && Character.isLowerCase(className.charAt(0));
}

@Override
public final String toString() {
if (!isGeneric()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,13 @@ public static int length(List<?> list) {
return list.size();
}

public static int length(SoyValue value) {
if (value instanceof SoyList) {
return ((SoyList) value).length();
}
return value.asJavaList().size();
}

@SuppressWarnings("deprecation") // we cannot do anything so go away
@Nonnull
public static SoyMap legacyObjectMapToMap(SoyValue value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@

package com.google.template.soy.basicfunctions;

import com.google.template.soy.plugin.java.restricted.JavaPluginContext;
import com.google.template.soy.plugin.java.restricted.JavaValue;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.restricted.JavaValueFactory;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptPluginContext;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValue;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValueFactory;
Expand All @@ -34,6 +33,7 @@
import com.google.template.soy.shared.restricted.SoyPureFunction;
import com.google.template.soy.shared.restricted.TypedSoyFunction;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;

/** Soy function that gets the length of a list. */
Expand All @@ -46,7 +46,7 @@
returnType = "int"))
@SoyFieldSignature(name = "length", baseType = "list<any>", returnType = "int")
public final class LengthFunction extends TypedSoyFunction
implements SoyJavaSourceFunction, SoyJavaScriptSourceFunction, SoyPythonSourceFunction {
implements SoyJavaScriptSourceFunction, SoyPythonSourceFunction, SoyJavaExternFunction {

@Override
public JavaScriptValue applyForJavaScriptSource(
Expand All @@ -62,13 +62,20 @@ public PythonValue applyForPythonSource(

// lazy singleton pattern, allows other backends to avoid the work.
private static final class Methods {
static final Method DELEGATE_SOYLIST_LENGTH =
JavaValueFactory.createMethod(BasicFunctionsRuntime.class, "length", List.class);
static final Method SOY_VALUE_LENGTH =
JavaValueFactory.createMethod(BasicFunctionsRuntime.class, "length", SoyValue.class);
static final Method COLLECTION_SIZE = JavaValueFactory.createMethod(Collection.class, "size");
}

@Override
public JavaValue applyForJavaSource(
JavaValueFactory factory, List<JavaValue> args, JavaPluginContext context) {
return factory.callStaticMethod(Methods.DELEGATE_SOYLIST_LENGTH, args.get(0));
public Method getExternJavaMethod(List<RuntimeType> argTypes) {
return argTypes.get(0) == RuntimeType.SOY_VALUE
? Methods.SOY_VALUE_LENGTH
: Methods.COLLECTION_SIZE;
}

@Override
public boolean adaptArgs() {
return false;
}
}
18 changes: 11 additions & 7 deletions java/src/com/google/template/soy/basicfunctions/SetSizeField.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
package com.google.template.soy.basicfunctions;

import com.google.template.soy.data.SoySet;
import com.google.template.soy.plugin.java.restricted.JavaPluginContext;
import com.google.template.soy.plugin.java.restricted.JavaValue;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.restricted.JavaValueFactory;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptPluginContext;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValue;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValueFactory;
Expand All @@ -32,13 +30,14 @@
import com.google.template.soy.shared.restricted.SoyFieldSignature;
import com.google.template.soy.shared.restricted.SoyPureFunction;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;

/** Soy field that gets the size of a set. */
@SoyFieldSignature(name = "size", baseType = "set<any>", returnType = "int")
@SoyPureFunction
public final class SetSizeField
implements SoyJavaSourceFunction, SoyJavaScriptSourceFunction, SoyPythonSourceFunction {
implements SoyJavaScriptSourceFunction, SoyPythonSourceFunction, SoyJavaExternFunction {

@Override
public JavaScriptValue applyForJavaScriptSource(
Expand All @@ -55,11 +54,16 @@ public PythonValue applyForPythonSource(
// lazy singleton pattern, allows other backends to avoid the work.
private static final class Methods {
static final Method SET_SIZE_FN = JavaValueFactory.createMethod(SoySet.class, "size");
static final Method COLLECTION_SIZE = JavaValueFactory.createMethod(Collection.class, "size");
}

@Override
public JavaValue applyForJavaSource(
JavaValueFactory factory, List<JavaValue> args, JavaPluginContext context) {
return factory.callJavaValueMethod(Methods.SET_SIZE_FN, args.get(0));
public Method getExternJavaMethod(List<RuntimeType> argTypes) {
return argTypes.get(0) == RuntimeType.SOY_VALUE ? Methods.SET_SIZE_FN : Methods.COLLECTION_SIZE;
}

@Override
public boolean adaptArgs() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
package com.google.template.soy.basicfunctions;

import com.google.template.soy.data.SoyValue;
import com.google.template.soy.plugin.java.restricted.JavaPluginContext;
import com.google.template.soy.plugin.java.restricted.JavaValue;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.restricted.JavaValueFactory;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptPluginContext;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValue;
import com.google.template.soy.plugin.javascript.restricted.JavaScriptValueFactory;
Expand All @@ -46,7 +44,7 @@
@SoyFieldSignature(name = "length", baseType = "string", returnType = "int")
@SoyPureFunction
public final class StrLenFunction
implements SoyJavaSourceFunction, SoyJavaScriptSourceFunction, SoyPythonSourceFunction {
implements SoyJavaScriptSourceFunction, SoyPythonSourceFunction, SoyJavaExternFunction {

@Override
public JavaScriptValue applyForJavaScriptSource(
Expand All @@ -62,13 +60,20 @@ public PythonValue applyForPythonSource(

// lazy singleton pattern, allows other backends to avoid the work.
private static final class Methods {
static final Method STR_LEN =
static final Method STRING_DATA_LENGTH =
JavaValueFactory.createMethod(BasicFunctionsRuntime.class, "strLen", SoyValue.class);
static final Method STRING_LENGTH = JavaValueFactory.createMethod(String.class, "length");
}

@Override
public JavaValue applyForJavaSource(
JavaValueFactory factory, List<JavaValue> args, JavaPluginContext context) {
return factory.callStaticMethod(Methods.STR_LEN, args.get(0));
public Method getExternJavaMethod(List<RuntimeType> argTypes) {
return argTypes.get(0) == RuntimeType.SOY_VALUE
? Methods.STRING_DATA_LENGTH
: Methods.STRING_LENGTH;
}

@Override
public boolean adaptArgs() {
return false;
}
}
32 changes: 25 additions & 7 deletions java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
import com.google.template.soy.jbcsrc.shared.ExtraConstantBootstraps;
import com.google.template.soy.jbcsrc.shared.Names;
import com.google.template.soy.plugin.java.internal.PluginAnalyzer;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.restricted.MethodSignature;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.shared.internal.BuiltinFunction;
Expand Down Expand Up @@ -1967,14 +1968,14 @@ SoyExpression visitNewSetFunction(FunctionNode node) {
@Override
SoyExpression visitPluginFunction(FunctionNode node) {
Object fn = node.getSoyFunction();
if (fn instanceof SoyJavaSourceFunction) {
if (fn instanceof SoyJavaExternFunction) {
ImmutableList<SoyExpression> args = visitAll(node.getParams());
Optional<Extern> externApi =
return callExtern(
ExternAdaptors.asExtern(
(SoyJavaSourceFunction) fn, args, node.getType(), node.getAllowedParamTypes());
if (externApi.isPresent()) {
return callExtern(externApi.get(), args);
}
(SoyJavaExternFunction) fn, args, node.getType(), node.getAllowedParamTypes()),
args);
} else if (fn instanceof SoyJavaSourceFunction) {
ImmutableList<SoyExpression> args = visitAll(node.getParams());
return sourceFunctionCompiler.compile(
node, (SoyJavaSourceFunction) fn, args, parameters, detacher);
} else if (fn instanceof Extern) {
Expand Down Expand Up @@ -2086,7 +2087,12 @@ private SoyExpression callExtern(Extern extern, List<SoyExpression> params) {
} else {
SoyType soyType = functionType.getParameters().get(soyArgIndex).getType();
SoyExpression soyArg = params.get(soyArgIndex);
args.add(ExternCompiler.adaptParameter(soyArg, paramType, soyType, parameters));
// Special path available to ExternSourceFunction to avoid any boxing or unboxing.
if (javaImpl.adaptArgs()) {
args.add(ExternCompiler.adaptParameter(soyArg, paramType, soyType, parameters));
} else {
args.add(adaptArbitraryMethodArg(soyArg, paramType));
}
soyArgIndex++;
}
}
Expand Down Expand Up @@ -2580,4 +2586,16 @@ static boolean requiresRenderContext(Extern extern) {
|| javaImpl.paramTypes().stream()
.anyMatch(t -> JavaImplNode.IMPLICIT_PARAMS.contains(t.className()));
}

private static Expression adaptArbitraryMethodArg(
Expression actualParam, TypeReference javaType) {
if (BytecodeUtils.isPrimitive(actualParam.resultType())) {
if (javaType.isPrimitive()) {
return actualParam;
} else {
return BytecodeUtils.boxJavaPrimitive(actualParam.resultType(), actualParam);
}
}
return actualParam.checkedCast(getTypeInfoForJavaImpl(javaType.className()).type());
}
}
44 changes: 35 additions & 9 deletions java/src/com/google/template/soy/jbcsrc/ExternAdaptors.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
import com.google.template.soy.base.SourceLogicalPath;
import com.google.template.soy.base.internal.TypeReference;
import com.google.template.soy.jbcsrc.restricted.SoyExpression;
import com.google.template.soy.jbcsrc.restricted.SoyRuntimeType;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction.RuntimeType;
import com.google.template.soy.plugin.restricted.SoySourceFunction;
import com.google.template.soy.shared.restricted.SoySourceFunctionMethod;
import com.google.template.soy.soytree.FileMetadata.Extern;
Expand All @@ -37,6 +38,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.objectweb.asm.Type;

class ExternAdaptors {
private ExternAdaptors() {}
Expand All @@ -53,6 +55,7 @@ public static Optional<Extern> asExtern(
asExtern(
(SoyJavaExternFunction) impl,
soyMethod.getMethodName(),
args,
soyMethod.getReturnType(),
ImmutableList.<SoyType>builder()
.add(soyMethod.getBaseType())
Expand All @@ -63,26 +66,28 @@ public static Optional<Extern> asExtern(
}

/**
* Adapts {@link SoyJavaSourceFunction} to {@link Extern} if the function's implementation is a
* Adapts {@link SoyJavaExternFunction} to {@link Extern} if the function's implementation is a
* {@link SoyJavaExternFunction}.
*/
public static Optional<Extern> asExtern(
SoyJavaSourceFunction fn,
public static Extern asExtern(
SoyJavaExternFunction fn,
List<SoyExpression> args,
SoyType returnType,
ImmutableList<SoyType> paramTypes) {
if (fn instanceof SoyJavaExternFunction) {
return Optional.of(asExtern((SoyJavaExternFunction) fn, "unused", returnType, paramTypes));
}
return Optional.empty();
return asExtern(fn, "unused", args, returnType, paramTypes);
}

private static Extern asExtern(
SoyJavaExternFunction impl,
String methodName,
List<SoyExpression> args,
SoyType returnType,
ImmutableList<SoyType> argTypes) {
Method method = impl.getExternJavaMethod();
ImmutableList<RuntimeType> argRuntimeTypes =
args.stream()
.map(a -> soyRuntimeTypeToExternApiType(a.soyRuntimeType()))
.collect(toImmutableList());
Method method = impl.getExternJavaMethod(argRuntimeTypes);
MethodType methodType;
if (Modifier.isStatic(method.getModifiers())) {
if (method.getDeclaringClass().isInterface()) {
Expand Down Expand Up @@ -135,6 +140,11 @@ public MethodType type() {
public boolean instanceFromContext() {
return false;
}

@Override
public boolean adaptArgs() {
return impl.adaptArgs();
}
};
return new Extern() {
@Override
Expand Down Expand Up @@ -168,4 +178,20 @@ public boolean isJavaAsync() {
}
};
}

private static RuntimeType soyRuntimeTypeToExternApiType(SoyRuntimeType soyRuntimeType) {
if (soyRuntimeType.isBoxed()) {
return RuntimeType.SOY_VALUE;
}
switch (soyRuntimeType.runtimeType().getSort()) {
case Type.LONG:
return RuntimeType.LONG;
case Type.DOUBLE:
return RuntimeType.DOUBLE;
case Type.BOOLEAN:
return RuntimeType.BOOLEAN;
default:
return RuntimeType.OBJECT;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,34 @@

import com.google.template.soy.plugin.restricted.SoySourceFunction;
import java.lang.reflect.Method;
import java.util.List;

/** A SoySourceFunction that can be executed in JBCSRC via the extern compilation pipeline. */
public interface SoyJavaExternFunction extends SoySourceFunction {

Method getExternJavaMethod();
/** The runtime type of an incoming argument. */
enum RuntimeType {
LONG,
DOUBLE,
BOOLEAN,
SOY_VALUE,
OBJECT
}

default Method getExternJavaMethod(List<RuntimeType> argTypes) {
return getExternJavaMethod();
}

default Method getExternJavaMethod() {
throw new AbstractMethodError();
}

/**
* Returns whether args should be adapted via the standard extern pipeline, or passed as-is. If
* this method returns false then typically getExternJavaMethod will return a method for boxed
* SoyValues and another method for unboxed values.
*/
default boolean adaptArgs() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
import com.google.template.soy.plugin.java.PluginInstances;
import com.google.template.soy.plugin.java.RenderCssHelper;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction;
import com.google.template.soy.plugin.java.internal.SoyJavaExternFunction.RuntimeType;
import com.google.template.soy.plugin.java.restricted.MethodSignature;
import com.google.template.soy.plugin.java.restricted.SoyJavaSourceFunction;
import com.google.template.soy.shared.SoyCssRenamingMap;
Expand Down Expand Up @@ -166,6 +167,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import javax.annotation.Nullable;

/**
Expand Down Expand Up @@ -1035,7 +1037,10 @@ SoyValue visitExtern(
ImmutableList<TofuJavaValue> args,
SoyType returnType,
ImmutableList<SoyType> argTypes) {
Method method = extern.getExternJavaMethod();
ImmutableList<RuntimeType> boxedArgs =
IntStream.of(args.size()).mapToObj(i -> RuntimeType.SOY_VALUE).collect(toImmutableList());

Method method = extern.getExternJavaMethod(boxedArgs);
SoyValue target = null;
if (!Modifier.isStatic(method.getModifiers())) {
target = args.get(0).soyValue();
Expand Down
Loading