Skip to content
Open
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
58 changes: 56 additions & 2 deletions java/src/com/google/template/soy/jbcsrc/ExpressionCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
import com.google.template.soy.jbcsrc.restricted.SoyRuntimeType;
import com.google.template.soy.jbcsrc.restricted.Statement;
import com.google.template.soy.jbcsrc.restricted.TypeInfo;
import com.google.template.soy.jbcsrc.runtime.JbcSrcExternRuntime;
import com.google.template.soy.jbcsrc.shared.ClassLoaderFallbackCallFactory;
import com.google.template.soy.jbcsrc.shared.ExtraConstantBootstraps;
import com.google.template.soy.jbcsrc.shared.Names;
Expand Down Expand Up @@ -151,11 +152,13 @@
import com.google.template.soy.types.MessageType;
import com.google.template.soy.types.MutableListType;
import com.google.template.soy.types.MutableMapType;
import com.google.template.soy.types.RecordType;
import com.google.template.soy.types.SetType;
import com.google.template.soy.types.SoyProtoEnumType;
import com.google.template.soy.types.SoyProtoType;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyTypes;
import com.google.template.soy.types.UnknownType;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
Expand Down Expand Up @@ -1764,6 +1767,48 @@ var record = (RecordLiteralNode) node.getChild(1);
baseExpr.checkedCast(BytecodeUtils.TEMPLATE_VALUE_TYPE),
recordLiteralAsParamStoreForBind(record)));
}
case INVOKE_FUNCTION_PROPERTY:
{
// We are compiling a dynamic function property invocation (e.g., `$myRecord.fn()`).
// 1. First, we extract the function object from the record at runtime.
// 2. We resolve the property to a SoyValue (which will be a JbcSrcFunctionValue).
// 3. We invoke the generic .call() interface on that function value.
Expression fieldProvider =
MethodRefs.RUNTIME_GET_FIELD_PROVIDER.invoke(
baseExpr.box(), constantRecordProperty(node.getMethodName().identifier()));
SoyExpression fieldExpr =
SoyExpression.forSoyValue(
UnknownType.getInstance(), detacher.resolveSoyValueProvider(fieldProvider));

SoyRuntimeType soyReturnType = ExternCompiler.getRuntimeType(node.getType());

FunctionType functionType = null;
if (node.getBaseExprChild().getType() instanceof RecordType recordType) {
SoyType propertyType = recordType.getMemberType(node.getMethodName().identifier());
if (propertyType instanceof FunctionType fType) {
functionType = fType;
}
}

Expression obj =
fieldExpr
.checkedCast(FUNCTION_VALUE_TYPE)
.invoke(
MethodRefs.FUNCTION_WITH_RENDER_CONTEXT, parameters.getRenderContext())
.invoke(
MethodRefs.FUNCTION_CALL,
adaptFunctionArgs(functionType, node.getParams()));

if (BytecodeUtils.isPrimitive(soyReturnType.runtimeType())) {
obj = BytecodeUtils.unboxJavaPrimitive(soyReturnType.runtimeType(), obj);
} else if (soyReturnType.runtimeType().equals(BytecodeUtils.SOY_VALUE_TYPE)) {
obj = JbcSrcExternRuntime.CONVERT_OBJECT_TO_SOY_VALUE.invoke(obj);
} else {
obj = obj.checkedCast(soyReturnType.runtimeType());
}

return SoyExpression.forRuntimeType(soyReturnType, obj);
}
}
} else if (function instanceof SoySourceFunctionMethod) {
SoySourceFunctionMethod sourceMethod = (SoySourceFunctionMethod) function;
Expand Down Expand Up @@ -2043,6 +2088,8 @@ SoyExpression visitPluginFunction(FunctionNode node) {
// relying on MethodHandle.invokeExact.
if (BytecodeUtils.isPrimitive(soyReturnType.runtimeType())) {
obj = BytecodeUtils.unboxJavaPrimitive(soyReturnType.runtimeType(), obj);
} else if (soyReturnType.runtimeType().equals(BytecodeUtils.SOY_VALUE_TYPE)) {
obj = JbcSrcExternRuntime.CONVERT_OBJECT_TO_SOY_VALUE.invoke(obj);
} else {
obj = obj.checkedCast(soyReturnType.runtimeType());
}
Expand All @@ -2064,11 +2111,18 @@ SoyExpression visitPluginFunction(FunctionNode node) {
.checkedSoyCast(node.getType()));
}

private Expression adaptFunctionArgs(FunctionType type, List<ExprNode> args) {
// The type may be null if the function was stored in an 'any' or '?' type (UNKNOWN).
// In that case, we don't know the exact parameter types, so we pass UnknownType
// to adaptExternArg, effectively disabling type-specific adaptation.
private Expression adaptFunctionArgs(@Nullable FunctionType type, List<ExprNode> args) {
List<Expression> adaptedArgs = new ArrayList<>(args.size());
for (int i = 0; i < args.size(); i++) {
ExprNode param = args.get(i);
SoyType paramType = type.getParameters().get(i).getType();
SoyType paramType = UnknownType.getInstance();
if (type != null) {
int index = Math.min(i, type.getParameters().size() - 1);
paramType = type.getParameters().get(index).getType();
}
Expression adapted = adaptExternArg(visit(param), paramType);
if (BytecodeUtils.isPrimitive(adapted.resultType())) {
adapted = BytecodeUtils.boxJavaPrimitive(adapted);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.google.common.collect.Iterables;
import com.google.template.soy.base.internal.FunctionalInterfaceUtil;
import com.google.template.soy.data.LoggingAdvisingAppendable;
import com.google.template.soy.data.SoyList;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueConverter;
import java.lang.invoke.MethodHandle;
Expand All @@ -31,6 +33,8 @@
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Map;

/** Runtime type for function pointers. */
@AutoValue
Expand Down Expand Up @@ -92,7 +96,56 @@ public Object call(ImmutableList<?> args) throws Throwable {
// Ignore extra arguments.
args = args.subList(0, paramCount);
}
return getHandle().invokeWithArguments(args);

Object[] adaptedArgs = new Object[args.size()];
for (int i = 0; i < args.size(); i++) {
Object arg = args.get(i);
Class<?> paramType = getHandle().type().parameterType(i);
// The incoming args are boxed SoyValues, but the underlying Java method might
// expect raw primitives or standard Java types. We unbox them if needed to
// match the exact signature of the target method before invoking it.
adaptedArgs[i] = unboxSoyValueIfNecessary(arg, paramType);
}
return getHandle().invokeWithArguments(adaptedArgs);
}

/**
* Unboxes a generic SoyValue argument into a specific Java type required by the underlying Java
* method signature (e.g., extracting the primitive `long` from an `IntegerData` box). If the
* method simply expects a SoyValue or Object, it returns the argument unchanged.
*/
private static Object unboxSoyValueIfNecessary(Object arg, Class<?> expectedType) {
if (!(arg instanceof SoyValue soyValue)) {
return arg;
}
if (expectedType == long.class || expectedType == Long.class) {
return soyValue.longValue();
}
if (expectedType == int.class || expectedType == Integer.class) {
return soyValue.integerValue();
}
if (expectedType == double.class || expectedType == Double.class) {
return soyValue.floatValue();
}
if (expectedType == float.class || expectedType == Float.class) {
return (float) soyValue.floatValue();
}
if (expectedType == boolean.class || expectedType == Boolean.class) {
return soyValue.booleanValue();
}
if (expectedType == String.class) {
return soyValue.stringValue();
}
if (expectedType == Number.class) {
return soyValue.numberValue();
}
if (List.class.isAssignableFrom(expectedType) && soyValue instanceof SoyList soyList) {
return soyList.asJavaList();
}
if (Map.class.isAssignableFrom(expectedType) && soyValue instanceof SoyMap soyMap) {
return soyMap.asJavaMap();
}
return arg;
}

public <T> T asInstance(Class<T> iface) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -806,10 +806,18 @@ private NullSafeAccumulator genCodeForMethodCall(
return base.transform(
nullSafe,
(baseExpr) -> genCodeForBind(baseExpr, visit(methodCallNode.getParam(0)), baseType));
case INVOKE_FUNCTION_PROPERTY:
// Compiles a dynamic function property invocation in JS (e.g. `$myRecord.fn()`).
// We compile this down to a standard JavaScript property access and call:
// `base.fn(...)`. FieldAccess.call handles exactly this.
return base.dotAccess(
FieldAccess.call(
methodCallNode.getMethodName().identifier(),
methodCallNode.getParams().stream().map(this::visit).collect(toImmutableList())),
nullSafe);
}
throw new AssertionError(builtinMethod);
} else if (soyMethod instanceof SoySourceFunctionMethod) {
SoySourceFunctionMethod sourceMethod = (SoySourceFunctionMethod) soyMethod;
} else if (soyMethod instanceof SoySourceFunctionMethod sourceMethod) {

return base.functionCall(
nullSafe,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2026 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.passes;

import com.google.template.soy.shared.restricted.SoyMethod;
import com.google.template.soy.types.FunctionType;
import com.google.template.soy.types.SoyType;
import javax.annotation.Nullable;

/**
* A specialized {@link SoyMethod} used during method resolution to represent invoking a
* function-typed property on a record.
*
* <p>This method does not leak to backend code generation; it is used solely within {@link
* ResolveExpressionTypesPass} to route method calls to function invocations.
*/
final class FunctionPropertyMethod implements SoyMethod {

@Nullable final FunctionType functionType;

FunctionPropertyMethod(@Nullable SoyType type) {
this.functionType = type instanceof FunctionType fnType ? fnType : null;
}

@Override
public int getNumArgs() {
return functionType == null ? -1 : functionType.getParameters().size();
}

@Override
public boolean acceptsArgCount(int count) {
if (functionType == null) {
return true;
}
return functionType.isVarArgs() ? count >= getNumArgs() - 1 : count == getNumArgs();
}
}
Loading