Skip to content

Commit fcbf710

Browse files
nicholasyu-googlecopybara-github
authored andcommitted
Implement lazy calls in the Jbc backend by turning them into NodeBuilder objects. When used inside of a let/param, they are stored without executing (i.e. "unflattened") so that printing a let/param containing a lazy call multiple times will re-execute them for each operation.
This is a no-op for all existing Soy, there are no gencode changes when there are no lazy calls. PiperOrigin-RevId: 764855479
1 parent 0d390a8 commit fcbf710

19 files changed

+440
-49
lines changed

java/src/com/google/template/soy/data/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ java_library(
113113
"//java/src/com/google/template/soy/internal/proto",
114114
"//java/src/com/google/template/soy/jbcsrc/api:helpers",
115115
"//java/src/com/google/template/soy/jbcsrc/shared:names",
116+
"//java/src/com/google/template/soy/jbcsrc/shared:stackframe",
116117
"@com_google_auto_value_auto_value",
117118
"@com_google_protobuf//:protobuf_java",
118119
"@maven//:com_google_code_findbugs_jsr305",

java/src/com/google/template/soy/data/LoggingAdvisingAppendable.java

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.google.template.soy.data.SanitizedContent.ContentKind;
2929
import com.google.template.soy.data.restricted.StringData;
3030
import com.google.template.soy.jbcsrc.api.AdvisingAppendable;
31+
import com.google.template.soy.jbcsrc.shared.StackFrame;
3132
import java.io.IOException;
3233
import java.util.ArrayList;
3334
import java.util.List;
@@ -202,6 +203,11 @@ public abstract LoggingAdvisingAppendable appendLoggingFunctionInvocation(
202203
LoggingFunctionInvocation funCall, ImmutableList<Function<String, String>> escapers)
203204
throws IOException;
204205

206+
@Nullable
207+
public StackFrame appendNodeBuilder(NodeBuilder nodeBuilder) throws IOException {
208+
return nodeBuilder.render(this);
209+
}
210+
205211
/** A buffer of commands that can be replayed on a {@link LoggingAdvisingAppendable}. */
206212
@Immutable
207213
public static final class CommandBuffer {
@@ -306,7 +312,7 @@ public LoggingAdvisingAppendable append(char c) throws IOException {
306312
/**
307313
* Returns the commands list, allocating it if necessary and appending any string data to it.
308314
*/
309-
private List<Object> getCommandsAndAddPendingStringData() {
315+
protected List<Object> getCommandsAndAddPendingStringData() {
310316
var commands = this.commands;
311317
if (commands == null) {
312318
this.commands = commands = new ArrayList<>();
@@ -377,6 +383,9 @@ private static void replayCommandOn(Object o, LoggingAdvisingAppendable appendab
377383
appendable.exitLoggableElement();
378384
} else if (o instanceof LogStatement) {
379385
appendable.enterLoggableElement((LogStatement) o);
386+
} else if (o instanceof NodeBuilder) {
387+
// TODO(b/421209829): Guarantee non-blocking by allowing a detach here.
388+
((NodeBuilder) o).renderBlocking(appendable);
380389
} else {
381390
throw new AssertionError("unexpected command object: " + o);
382391
}
@@ -469,12 +478,70 @@ private static void appendCommandToBuilder(Object command, StringBuilder builder
469478
builder.append(
470479
escapePlaceholder(
471480
loggingFunctionCommand.fn().placeholderValue(), loggingFunctionCommand.escapers()));
481+
} else if (command instanceof NodeBuilder) {
482+
// TODO(b/421209829): Guarantee non-blocking by allowing a detach here.
483+
((NodeBuilder) command).renderBlocking(new DelegatingAppendable(builder));
472484
}
473485
// ignore the logging statements
474486

475487
}
476488
}
477489

490+
/** Wraps an Appendable. Ignores logs. */
491+
static class DelegatingAppendable extends LoggingAdvisingAppendable {
492+
private final Appendable outputAppendable;
493+
494+
DelegatingAppendable(Appendable outputAppendable) {
495+
this.outputAppendable = checkNotNull(outputAppendable);
496+
}
497+
498+
@Override
499+
public boolean softLimitReached() {
500+
return false;
501+
}
502+
503+
@CanIgnoreReturnValue
504+
@Override
505+
public LoggingAdvisingAppendable append(CharSequence csq) throws IOException {
506+
outputAppendable.append(csq);
507+
return this;
508+
}
509+
510+
@CanIgnoreReturnValue
511+
@Override
512+
public LoggingAdvisingAppendable append(CharSequence csq, int start, int end)
513+
throws IOException {
514+
outputAppendable.append(csq, start, end);
515+
return this;
516+
}
517+
518+
@CanIgnoreReturnValue
519+
@Override
520+
public LoggingAdvisingAppendable append(char c) throws IOException {
521+
outputAppendable.append(c);
522+
return this;
523+
}
524+
525+
@CanIgnoreReturnValue
526+
@Override
527+
public LoggingAdvisingAppendable appendLoggingFunctionInvocation(
528+
LoggingFunctionInvocation funCall, ImmutableList<Function<String, String>> escapers) {
529+
return this;
530+
}
531+
532+
@CanIgnoreReturnValue
533+
@Override
534+
public LoggingAdvisingAppendable enterLoggableElement(LogStatement statement) {
535+
return this;
536+
}
537+
538+
@CanIgnoreReturnValue
539+
@Override
540+
public LoggingAdvisingAppendable exitLoggableElement() {
541+
return this;
542+
}
543+
}
544+
478545
@AutoValue
479546
abstract static class LoggingFunctionCommand {
480547
static LoggingFunctionCommand create(
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2025 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.template.soy.data;
18+
19+
import com.google.template.soy.jbcsrc.api.RenderResult;
20+
import com.google.template.soy.jbcsrc.shared.StackFrame;
21+
import java.io.IOException;
22+
import java.io.UncheckedIOException;
23+
import java.lang.invoke.CallSite;
24+
import java.util.ArrayList;
25+
import java.util.Collections;
26+
27+
/** A lazy call. */
28+
public class NodeBuilder {
29+
30+
/** Factory bound to a CallSite. */
31+
@SuppressWarnings("AvoidObjectArrays")
32+
public static interface Builder {
33+
public NodeBuilder build(StackFrame stackFrame, Object[] templateParams, Object renderContext);
34+
}
35+
36+
public static Builder builder(CallSite callSite) {
37+
return (StackFrame stackFrame, Object[] templateParams, Object renderContext) ->
38+
new NodeBuilder(callSite, stackFrame, templateParams, renderContext);
39+
}
40+
41+
private final CallSite callSite;
42+
private final StackFrame stackFrame;
43+
private final Object[] templateParams;
44+
// Type is always com.google.template.soy.jbcsrc.shared.RenderContext, declared as Object to avoid
45+
// circular dep.
46+
private final Object renderContext;
47+
48+
@SuppressWarnings("AvoidObjectArrays")
49+
public NodeBuilder(
50+
CallSite callSite, StackFrame stackFrame, Object[] templateParams, Object renderContext) {
51+
this.callSite = callSite;
52+
this.stackFrame = stackFrame;
53+
this.templateParams = templateParams;
54+
this.renderContext = renderContext;
55+
}
56+
57+
public StackFrame render(LoggingAdvisingAppendable appendable) {
58+
ArrayList<Object> params = new ArrayList<>();
59+
params.add(stackFrame);
60+
Collections.addAll(params, templateParams);
61+
params.add(appendable);
62+
params.add(renderContext);
63+
try {
64+
return (StackFrame) callSite.getTarget().invokeWithArguments(params);
65+
} catch (Throwable e) {
66+
throw new IllegalArgumentException("Unexpected error while calling " + callSite, e);
67+
}
68+
}
69+
70+
public void renderBlocking(LoggingAdvisingAppendable appendable) {
71+
NodeBuilder builder = this;
72+
do {
73+
StackFrame newFrame;
74+
try {
75+
newFrame = appendable.appendNodeBuilder(builder);
76+
} catch (IOException e) {
77+
throw new UncheckedIOException(e);
78+
}
79+
if (newFrame == null) {
80+
return;
81+
}
82+
RenderResult result = newFrame.asRenderResult();
83+
if (result.type() == RenderResult.Type.DETACH) {
84+
result.resolveDetach();
85+
}
86+
builder = new NodeBuilder(callSite, newFrame, templateParams, renderContext);
87+
} while (true);
88+
}
89+
}

java/src/com/google/template/soy/jbcsrc/AppendableExpression.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.template.soy.data.LogStatement;
3030
import com.google.template.soy.data.LoggingAdvisingAppendable;
3131
import com.google.template.soy.data.LoggingFunctionInvocation;
32+
import com.google.template.soy.data.NodeBuilder;
3233
import com.google.template.soy.data.SanitizedContent.ContentKind;
3334
import com.google.template.soy.jbcsrc.restricted.BytecodeUtils;
3435
import com.google.template.soy.jbcsrc.restricted.CodeBuilder;
@@ -155,6 +156,9 @@ static Statement concat(List<Statement> statements) {
155156
MethodRef.createPure(
156157
LoggingFunctionInvocation.class, "create", String.class, String.class, List.class);
157158

159+
private static final MethodRef APPEND_NODE_BUILDER =
160+
MethodRef.createPure(LoggingAdvisingAppendable.class, "appendNodeBuilder", NodeBuilder.class);
161+
158162
private static final MethodRef SET_SANITIZED_CONTENT_KIND_AND_DIRECTIONALITY =
159163
MethodRef.createNonPure(
160164
LoggingAdvisingAppendable.class, "setKindAndDirectionality", ContentKind.class)
@@ -263,6 +267,11 @@ AppendableExpression appendLoggingFunctionInvocation(
263267
BytecodeUtils.asImmutableList(escapingDirectives)));
264268
}
265269

270+
/** Invokes {@link LoggingAdvisingAppendable#appendNodeBuilde} */
271+
Expression appendNodeBuilder(Expression exp) {
272+
return APPEND_NODE_BUILDER.invoke(this, exp);
273+
}
274+
266275
/** Invokes {@link LoggingAdvisingAppendable#setSanitizedContentKind} on the appendable. */
267276
AppendableExpression setSanitizedContentKindAndDirectionality(SanitizedContentKind kind) {
268277
return withNewDelegate(

java/src/com/google/template/soy/jbcsrc/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ java_library(
4040
"//java/src/com/google/template/soy/jbcsrc/runtime:more_runtime",
4141
"//java/src/com/google/template/soy/jbcsrc/shared",
4242
"//java/src/com/google/template/soy/jbcsrc/shared:names",
43+
"//java/src/com/google/template/soy/jbcsrc/shared:stackframe",
4344
"//java/src/com/google/template/soy/logging:internal",
4445
"//java/src/com/google/template/soy/logging:public",
4546
"//java/src/com/google/template/soy/msgs",

java/src/com/google/template/soy/jbcsrc/SoyNodeCompiler.java

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1484,6 +1484,17 @@ protected Statement visitCallDelegateNode(CallDelegateNode node) {
14841484
String.class)
14851485
.asHandle();
14861486

1487+
private static final Handle STATIC_NODEBUILDER_HANDLE =
1488+
MethodRef.createPure(
1489+
ClassLoaderFallbackCallFactory.class,
1490+
"bootstrapNodeBuilder",
1491+
MethodHandles.Lookup.class,
1492+
String.class,
1493+
MethodType.class,
1494+
String.class,
1495+
String.class)
1496+
.asHandle();
1497+
14871498
private static final Handle STATIC_TEMPLATE_HANDLE =
14881499
MethodRef.createPure(
14891500
ClassLoaderFallbackCallFactory.class,
@@ -1549,6 +1560,30 @@ public Expression call(
15491560
List<Expression> params,
15501561
AppendableExpression appendable,
15511562
RenderContextExpression renderContext) {
1563+
if (node.isLazy()) {
1564+
Expression nbParams =
1565+
BytecodeUtils.asArray(
1566+
Type.getType(Object[].class), ImmutableList.copyOf(params));
1567+
Expression nodeBuilder =
1568+
new Expression(BytecodeUtils.NODE_BUILDER_TYPE) {
1569+
@Override
1570+
protected void doGen(CodeBuilder adapter) {
1571+
stackFrame.gen(adapter);
1572+
nbParams.gen(adapter);
1573+
renderContext.gen(adapter);
1574+
adapter.visitInvokeDynamicInsn(
1575+
"bootstrapNodeBuilder",
1576+
"(Lcom/google/template/soy/jbcsrc/shared/StackFrame;"
1577+
+ "[Ljava/lang/Object;"
1578+
+ "Ljava/lang/Object;)"
1579+
+ "Lcom/google/template/soy/data/NodeBuilder;",
1580+
STATIC_NODEBUILDER_HANDLE,
1581+
node.getCalleeName(),
1582+
positionalRenderMethod.method().getDescriptor());
1583+
}
1584+
};
1585+
return appendableExpression.appendNodeBuilder(nodeBuilder);
1586+
}
15521587
if (isPrivateCall) {
15531588
return positionalRenderMethod.invoke(
15541589
ImmutableList.<Expression>builder()
@@ -1582,6 +1617,30 @@ protected void doGen(CodeBuilder adapter) {
15821617
public Optional<DirectCallGenerator> asDirectCall() {
15831618
return Optional.of(
15841619
(stackFrame, params, appendable, renderContext) -> {
1620+
if (node.isLazy()) {
1621+
Expression nbParams =
1622+
BytecodeUtils.asArray(
1623+
Type.getType(Object[].class), ImmutableList.of(params));
1624+
Expression nodeBuilder =
1625+
new Expression(BytecodeUtils.NODE_BUILDER_TYPE) {
1626+
@Override
1627+
protected void doGen(CodeBuilder adapter) {
1628+
stackFrame.gen(adapter);
1629+
nbParams.gen(adapter);
1630+
renderContext.gen(adapter);
1631+
adapter.visitInvokeDynamicInsn(
1632+
"bootstrapNodeBuilder",
1633+
"(Lcom/google/template/soy/jbcsrc/shared/StackFrame;"
1634+
+ "[Ljava/lang/Object;"
1635+
+ "Ljava/lang/Object;)"
1636+
+ "Lcom/google/template/soy/data/NodeBuilder;",
1637+
STATIC_NODEBUILDER_HANDLE,
1638+
node.getCalleeName(),
1639+
metadata.renderMethod().method().getDescriptor());
1640+
}
1641+
};
1642+
return appendableExpression.appendNodeBuilder(nodeBuilder);
1643+
}
15851644
if (isPrivateCall) {
15861645
return metadata
15871646
.renderMethod()
@@ -1646,6 +1705,7 @@ private Statement renderCallNode(CallNode node, CallGenerator callGenerator) {
16461705
TemplateVariableManager.Scope renderScope = variables.enterScope();
16471706
Statement initCallee = Statement.NULL_STATEMENT;
16481707
boolean allPrintDirectivesStreamable = areAllPrintDirectivesStreamable(node);
1708+
boolean isLazy = (node instanceof CallBasicNode) && ((CallBasicNode) node).isLazy();
16491709
if (!allPrintDirectivesStreamable || node.isErrorFallbackSkip()) {
16501710
// in this case we need to wrap a CompiledTemplate to buffer to catch exceptions or to
16511711
// apply non-streaming escaping directives.
@@ -1668,15 +1728,26 @@ private Statement renderCallNode(CallNode node, CallGenerator callGenerator) {
16681728
TemplateVariableManager.SaveStrategy.STORE);
16691729
initCallee = calleeVariable.initializer();
16701730
boundCall =
1671-
(frame, output, context) ->
1672-
calleeVariable
1731+
(frame, output, context) -> {
1732+
if (isLazy) {
1733+
Expression nodeBuilder =
1734+
MethodRefs.CREATE_NODE_BUILDER.invoke(
1735+
calleeVariable.accessor(),
1736+
frame,
1737+
expressionAndInitializer.expression(),
1738+
context);
1739+
return output.appendNodeBuilder(nodeBuilder);
1740+
} else {
1741+
return calleeVariable
16731742
.accessor()
16741743
.invoke(
16751744
MethodRefs.COMPILED_TEMPLATE_RENDER,
16761745
frame,
16771746
expressionAndInitializer.expression(),
16781747
output,
16791748
context);
1749+
}
1750+
};
16801751
} else {
16811752
Optional<DirectPositionalCallGenerator> asDirectPositionalCall =
16821753
callGenerator.asDirectPositionalCall();
@@ -1716,18 +1787,30 @@ private Statement renderCallNode(CallNode node, CallGenerator callGenerator) {
17161787
TemplateVariableManager.SaveStrategy.STORE);
17171788
initCallee = calleeVariable.initializer();
17181789
boundCall =
1719-
(frame, output, context) ->
1720-
calleeVariable
1790+
(frame, output, context) -> {
1791+
if (isLazy) {
1792+
Expression nodeBuilder =
1793+
MethodRefs.CREATE_NODE_BUILDER.invoke(
1794+
calleeVariable.accessor(),
1795+
frame,
1796+
expressionAndInitializer.expression(),
1797+
context);
1798+
return output.appendNodeBuilder(nodeBuilder);
1799+
} else {
1800+
return calleeVariable
17211801
.accessor()
17221802
.invoke(
17231803
MethodRefs.COMPILED_TEMPLATE_RENDER,
17241804
frame,
17251805
expressionAndInitializer.expression(),
17261806
output,
17271807
context);
1808+
}
1809+
};
17281810
}
17291811
}
17301812
}
1813+
17311814
if (!node.getEscapingDirectives().isEmpty() && allPrintDirectivesStreamable) {
17321815
PrintDirectives.AppendableAndFlushBuffersDepth wrappedAppendable =
17331816
applyStreamingEscapingDirectives(

0 commit comments

Comments
 (0)