/*
* Copyright 2015 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;
import static com.google.common.base.Predicates.notNull;
import static com.google.template.soy.jbcsrc.BytecodeUtils.ADVISING_APPENDABLE_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.NULLARY_INIT;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.FieldRef.createField;
import static com.google.template.soy.jbcsrc.LocalVariable.createLocal;
import static com.google.template.soy.jbcsrc.LocalVariable.createThisVar;
import static com.google.template.soy.jbcsrc.MethodRef.RENDER_RESULT_DONE;
import static com.google.template.soy.jbcsrc.StandardNames.IJ_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.PARAMS_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.RENDER_CONTEXT_FIELD;
import static com.google.template.soy.jbcsrc.StandardNames.STATE_FIELD;
import static com.google.template.soy.jbcsrc.Statement.returnExpression;
import static com.google.template.soy.soytree.SoyTreeUtils.isDescendantOf;
import static java.util.Arrays.asList;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.collect.Iterables;
import com.google.template.soy.base.internal.UniqueNameGenerator;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.jbcsrc.SoyNodeCompiler.CompiledMethodBody;
import com.google.template.soy.jbcsrc.api.AdvisingAppendable;
import com.google.template.soy.jbcsrc.runtime.DetachableContentProvider;
import com.google.template.soy.jbcsrc.runtime.DetachableSoyValueProvider;
import com.google.template.soy.jbcsrc.shared.RenderContext;
import com.google.template.soy.soytree.CallParamContentNode;
import com.google.template.soy.soytree.CallParamValueNode;
import com.google.template.soy.soytree.LetContentNode;
import com.google.template.soy.soytree.LetValueNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.defn.LocalVar;
import com.google.template.soy.soytree.defn.TemplateParam;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.Method;
/**
* A compiler for lazy closures.
*
* <p>Certain Soy operations trigger lazy execution, in particular {@code {let ...}} and {@code
* {param ...}} statements. This laziness allows Soy rendering to both limit the amount of temporary
* buffers that must be used as well as to delay evaluating expressions until the results are needed
* (expression evaluation may trigger detaches).
*
* <p>There are 2 kinds of lazy execution:
*
* <ul>
* <li>Lazy expression evaluation. Triggered by {@link LetValueNode} or {@link
* CallParamValueNode}. For each of these we will generate a subtype of {@link
* DetachableSoyValueProvider}.
* <li>Lazy content evaluation. Triggered by {@link LetContentNode} or {@link
* CallParamContentNode}. For each of these we will generate a subtype of {@link
* DetachableContentProvider} and appropriately wrap it around a {@link SanitizedContent} or
* {@link StringData} value.
* </ul>
*
* <p>Each of these lazy statements execute in the context of their parents and have access to all
* the local variables and parameters of their parent templates at the point of their definition. To
* implement this, the child will be passed references to all needed data explicitly at the point of
* definition. To do this we will identify all the data that will be referenced by the closure and
* pass it as explicit constructor parameters and will store them in fields. So that, for a template
* like:
*
* <pre>{@code
* {template .foo}
* {{@literal @}param a : int}
* {let b : $a + 1 /}
* {$b}
* {/template}
* }</pre>
*
* <p>The compiled result will look something like:
*
* <pre>{@code
* ...
* LetValue$$b b = new LetValue$$b(params.getFieldProvider("a"));
* b.render(out);
* ...
*
* final class LetValue$$b extends DetachableSoyValueProvider {
* final SoyValueProvider a;
* LetValue$$b(SoyValueProvider a) {
* this.a = a;
* }
*
* {@literal @}Override protected RenderResult doResolve() {
* this.resolvedValue = eval(expr, node);
* return RenderResult.done();
* }
* }
* }</pre>
*/
final class LazyClosureCompiler {
// All our lazy closures are package private. They should only be referenced by their parent
// classes (or each other)
private static final int LAZY_CLOSURE_ACCESS = Opcodes.ACC_FINAL;
private static final Method DO_RESOLVE;
private static final Method DO_RENDER;
private static final Method DETACHABLE_CONTENT_PROVIDER_INIT;
private static final FieldRef RESOLVED_VALUE =
FieldRef.instanceFieldReference(DetachableSoyValueProvider.class, "resolvedValue");
private static final TypeInfo DETACHABLE_CONTENT_PROVIDER_TYPE =
TypeInfo.create(DetachableContentProvider.class);
private static final TypeInfo DETACHABLE_VALUE_PROVIDER_TYPE =
TypeInfo.create(DetachableSoyValueProvider.class);
static {
try {
DO_RESOLVE =
Method.getMethod(DetachableSoyValueProvider.class.getDeclaredMethod("doResolve"));
DO_RENDER =
Method.getMethod(
DetachableContentProvider.class.getDeclaredMethod(
"doRender", AdvisingAppendable.class));
DETACHABLE_CONTENT_PROVIDER_INIT =
Method.getMethod(
DetachableContentProvider.class.getDeclaredConstructor(ContentKind.class));
} catch (NoSuchMethodException | SecurityException e) {
throw new AssertionError(e);
}
}
private final CompiledTemplateRegistry registry;
private final InnerClasses innerClasses;
private final TemplateParameterLookup parentVariableLookup;
private final ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler;
private final TemplateVariableManager parentVariables;
LazyClosureCompiler(
CompiledTemplateRegistry registry,
InnerClasses innerClasses,
TemplateParameterLookup parentVariableLookup,
TemplateVariableManager parentVariables,
ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler) {
this.registry = registry;
this.innerClasses = innerClasses;
this.parentVariableLookup = parentVariableLookup;
this.parentVariables = parentVariables;
this.expressionToSoyValueProviderCompiler = expressionToSoyValueProviderCompiler;
}
Expression compileLazyExpression(
String namePrefix, SoyNode declaringNode, String varName, ExprNode exprNode) {
Optional<Expression> asSoyValueProvider =
expressionToSoyValueProviderCompiler.compileAvoidingDetaches(exprNode);
if (asSoyValueProvider.isPresent()) {
return asSoyValueProvider.get();
}
TypeInfo type =
innerClasses.registerInnerClassWithGeneratedName(
getProposedName(namePrefix, varName), LAZY_CLOSURE_ACCESS);
SoyClassWriter writer =
SoyClassWriter.builder(type)
.setAccess(LAZY_CLOSURE_ACCESS)
.extending(DETACHABLE_VALUE_PROVIDER_TYPE)
.sourceFileName(declaringNode.getSourceLocation().getFileName())
.build();
Expression expr =
new CompilationUnit(writer, type, DETACHABLE_VALUE_PROVIDER_TYPE, declaringNode)
.compileExpression(exprNode);
innerClasses.registerAsInnerClass(writer, type);
writer.visitEnd();
innerClasses.add(writer.toClassData());
return expr;
}
Expression compileLazyContent(String namePrefix, RenderUnitNode renderUnit, String varName) {
Optional<Expression> asRawText = asRawTextOnly(renderUnit);
if (asRawText.isPresent()) {
return asRawText.get();
}
TypeInfo type =
innerClasses.registerInnerClassWithGeneratedName(
getProposedName(namePrefix, varName), LAZY_CLOSURE_ACCESS);
SoyClassWriter writer =
SoyClassWriter.builder(type)
.setAccess(LAZY_CLOSURE_ACCESS)
.extending(DETACHABLE_CONTENT_PROVIDER_TYPE)
.sourceFileName(renderUnit.getSourceLocation().getFileName())
.build();
Expression expr =
new CompilationUnit(writer, type, DETACHABLE_CONTENT_PROVIDER_TYPE, renderUnit)
.compileRenderable(renderUnit);
innerClasses.registerAsInnerClass(writer, type);
writer.visitEnd();
innerClasses.add(writer.toClassData());
return expr;
}
private Optional<Expression> asRawTextOnly(RenderUnitNode renderUnit) {
StringBuilder builder = null;
for (StandaloneNode child : renderUnit.getChildren()) {
if (child instanceof RawTextNode) {
if (builder == null) {
builder = new StringBuilder();
}
builder.append(((RawTextNode) child).getRawText());
} else {
return Optional.absent();
}
}
// TODO(lukes): ideally this would be a static final StringData field rather than reboxing each
// time, but we don't (yet) have a good mechanism for that.
ContentKind kind = renderUnit.getContentKind();
Expression constant = constant(builder == null ? "" : builder.toString(), parentVariables);
if (kind == null) {
return Optional.<Expression>of(MethodRef.STRING_DATA_FOR_VALUE.invoke(constant));
} else {
return Optional.<Expression>of(
MethodRef.ORDAIN_AS_SAFE.invoke(constant, FieldRef.enumReference(kind).accessor()));
}
}
private String getProposedName(String prefix, String varName) {
return prefix + "_" + varName;
}
/** A simple object to aid in generating code for a single node. */
private final class CompilationUnit {
final UniqueNameGenerator fieldNames = JbcSrcNameGenerators.forFieldNames();
final TypeInfo type;
final TypeInfo baseClass;
final SoyNode node;
final SoyClassWriter writer;
CompilationUnit(SoyClassWriter writer, TypeInfo type, TypeInfo baseClass, SoyNode node) {
this.writer = writer;
this.type = type;
this.baseClass = baseClass;
this.node = node;
}
Expression compileExpression(ExprNode exprNode) {
final Label start = new Label();
final Label end = new Label();
final LocalVariable thisVar = createThisVar(type, start, end);
TemplateVariableManager variableSet =
new TemplateVariableManager(fieldNames, type, thisVar, DO_RESOLVE);
LazyClosureParameterLookup lookup =
new LazyClosureParameterLookup(this, parentVariableLookup, variableSet, thisVar);
SoyExpression compile =
ExpressionCompiler.createBasicCompiler(lookup, variableSet).compile(exprNode);
SoyExpression expression = compile.box();
final Statement storeExpr =
RESOLVED_VALUE
.putInstanceField(thisVar, expression)
.withSourceLocation(exprNode.getSourceLocation());
final Statement returnDone = Statement.returnExpression(RENDER_RESULT_DONE.invoke());
Statement doResolveImpl =
new Statement() {
@Override
void doGen(CodeBuilder adapter) {
adapter.mark(start);
storeExpr.gen(adapter);
returnDone.gen(adapter);
adapter.mark(end);
}
};
variableSet.defineStaticFields(writer);
Statement fieldInitializers = variableSet.defineFields(writer);
Expression constructExpr =
generateConstructor(
new Statement() {
@Override
void doGen(CodeBuilder adapter) {
adapter.loadThis();
adapter.invokeConstructor(baseClass.type(), NULLARY_INIT);
}
},
fieldInitializers,
lookup.getCapturedFields());
doResolveImpl.writeMethod(Opcodes.ACC_PROTECTED, DO_RESOLVE, writer);
return constructExpr;
}
Expression compileRenderable(RenderUnitNode renderUnit) {
FieldRef stateField = createField(type, STATE_FIELD, Type.INT_TYPE);
stateField.defineField(writer);
fieldNames.claimName(STATE_FIELD);
final Label start = new Label();
final Label end = new Label();
final LocalVariable thisVar = createThisVar(type, start, end);
final LocalVariable appendableVar =
createLocal("appendable", 1, ADVISING_APPENDABLE_TYPE, start, end).asNonNullable();
final TemplateVariableManager variableSet =
new TemplateVariableManager(fieldNames, type, thisVar, DO_RENDER);
LazyClosureParameterLookup lookup =
new LazyClosureParameterLookup(this, parentVariableLookup, variableSet, thisVar);
SoyNodeCompiler soyNodeCompiler =
SoyNodeCompiler.create(
registry,
innerClasses,
stateField,
thisVar,
AppendableExpression.forLocal(appendableVar),
variableSet,
lookup);
CompiledMethodBody compileChildren = soyNodeCompiler.compileChildren(renderUnit);
writer.setNumDetachStates(compileChildren.numberOfDetachStates());
final Statement nodeBody = compileChildren.body();
final Statement returnDone = returnExpression(MethodRef.RENDER_RESULT_DONE.invoke());
Statement fullMethodBody =
new Statement() {
@Override
void doGen(CodeBuilder adapter) {
adapter.mark(start);
nodeBody.gen(adapter);
adapter.mark(end);
returnDone.gen(adapter);
thisVar.tableEntry(adapter);
appendableVar.tableEntry(adapter);
variableSet.generateTableEntries(adapter);
}
};
ContentKind kind = renderUnit.getContentKind();
final Expression contentKind = BytecodeUtils.constant(kind);
variableSet.defineStaticFields(writer);
Statement fieldInitializers = variableSet.defineFields(writer);
Statement superClassContstructor =
new Statement() {
@Override
void doGen(CodeBuilder adapter) {
adapter.loadThis();
contentKind.gen(adapter);
adapter.invokeConstructor(baseClass.type(), DETACHABLE_CONTENT_PROVIDER_INIT);
}
};
Expression constructExpr =
generateConstructor(
superClassContstructor, fieldInitializers, lookup.getCapturedFields());
fullMethodBody.writeMethod(Opcodes.ACC_PROTECTED, DO_RENDER, writer);
return constructExpr;
}
/**
* Generates a public constructor that assigns our final field and checks for missing required
* params and returns an expression invoking that constructor with
*
* <p>This constructor is called by the generate factory classes.
*/
Expression generateConstructor(
final Statement superClassConstructorInvocation,
final Statement fieldInitializers,
Iterable<ParentCapture> captures) {
final Label start = new Label();
final Label end = new Label();
final LocalVariable thisVar = createThisVar(type, start, end);
final List<LocalVariable> params = new ArrayList<>();
List<Type> paramTypes = new ArrayList<>();
final List<Statement> assignments = new ArrayList<>();
final List<Expression> argExpressions = new ArrayList<>();
int index = 1; // start at 1 since 'this' occupied slot 0
for (ParentCapture capture : captures) {
FieldRef field = capture.field();
field.defineField(writer);
LocalVariable var = createLocal(field.name(), index, field.type(), start, end);
assignments.add(field.putInstanceField(thisVar, var));
argExpressions.add(capture.parentExpression());
params.add(var);
paramTypes.add(field.type());
index += field.type().getSize();
}
Statement constructorBody =
new Statement() {
@Override
void doGen(CodeBuilder cb) {
cb.mark(start);
// call super()
superClassConstructorInvocation.gen(cb);
// init fields
fieldInitializers.gen(cb);
// assign params to fields
for (Statement assignment : assignments) {
assignment.gen(cb);
}
cb.returnValue();
cb.mark(end);
thisVar.tableEntry(cb);
for (LocalVariable local : params) {
local.tableEntry(cb);
}
}
};
ConstructorRef constructor = ConstructorRef.create(type, paramTypes);
constructorBody.writeMethod(Opcodes.ACC_PUBLIC, constructor.method(), writer);
return constructor.construct(argExpressions);
}
}
/**
* Represents a field captured from our parent. To capture a value from our parent we grab the
* expression that produces that value and then generate a field in the child with the same type.
*
* <p>{@link CompilationUnit#generateConstructor(Iterable)} generates the code to propagate the
* captured values from the parent to the child, and from the constructor to the generated fields.
*/
@AutoValue
abstract static class ParentCapture {
static ParentCapture create(TypeInfo owner, String name, Expression parentExpression) {
FieldRef captureField = FieldRef.createFinalField(owner, name, parentExpression.resultType());
if (parentExpression.isNonNullable()) {
captureField = captureField.asNonNull();
}
return new AutoValue_LazyClosureCompiler_ParentCapture(captureField, parentExpression);
}
/** The field in the closure that stores the captured value. */
abstract FieldRef field();
/** An expression that produces the value for this capture from the parent. */
abstract Expression parentExpression();
}
/**
* The {@link LazyClosureParameterLookup} will generate expressions for all variable references
* within a lazy closure. The strategy is simple
*
* <ul>
* <li>If the variable is a template parameter, query the parent variable lookup and generate a
* {@link ParentCapture} for it
* <li>If the variable is a local (synthetic or otherwise), check if the declaring node is a
* descendant of the current lazy node. If it is, generate code for a normal variable lookup
* (via our own VariableSet), otherwise generate a {@link ParentCapture} to grab the value
* from our parent.
* <li>Finally, for the {@link RenderContext}, we lazily generate a {@link ParentCapture} if
* necessary.
* </ul>
*/
private static final class LazyClosureParameterLookup implements TemplateParameterLookup {
private final CompilationUnit params;
private final TemplateParameterLookup parentParameterLookup;
private final TemplateVariableManager variableSet;
private final Expression thisVar;
// These fields track all the parent captures that we need to generate.
// NOTE: TemplateParam and LocalVar have identity semantics. But the AST is guaranteed to not
// have multiple copies.
private final Map<TemplateParam, ParentCapture> paramFields = new LinkedHashMap<>();
private final Map<LocalVar, ParentCapture> localFields = new LinkedHashMap<>();
private final Map<SyntheticVarName, ParentCapture> syntheticFields = new LinkedHashMap<>();
private ParentCapture renderContextCapture;
private ParentCapture paramsCapture;
private ParentCapture ijCapture;
LazyClosureParameterLookup(
CompilationUnit params,
TemplateParameterLookup parentParameterLookup,
TemplateVariableManager variableSet,
Expression thisVar) {
this.params = params;
this.parentParameterLookup = parentParameterLookup;
this.variableSet = variableSet;
this.thisVar = thisVar;
}
@Override
public Expression getParam(TemplateParam param) {
// All params are packed into fields
ParentCapture capturedField = paramFields.get(param);
if (capturedField == null) {
String name = param.name();
params.fieldNames.claimName(name);
capturedField =
ParentCapture.create(params.type, name, parentParameterLookup.getParam(param));
paramFields.put(param, capturedField);
}
return capturedField.field().accessor(thisVar);
}
@Override
public Expression getLocal(LocalVar local) {
if (isDescendantOf(local.declaringNode(), params.node)) {
// in this case, we just delegate to VariableSet
return variableSet.getVariable(local.name()).local();
}
ParentCapture capturedField = localFields.get(local);
if (capturedField == null) {
String name = params.fieldNames.generateName(local.name());
capturedField =
ParentCapture.create(params.type, name, parentParameterLookup.getLocal(local));
localFields.put(local, capturedField);
}
return capturedField.field().accessor(thisVar);
}
@Override
public Expression getLocal(SyntheticVarName varName) {
if (isDescendantOf(varName.declaringNode(), params.node)) {
// in this case, we just delegate to VariableSet
return variableSet.getVariable(varName).local();
}
ParentCapture capturedField = syntheticFields.get(varName);
if (capturedField == null) {
String name = params.fieldNames.generateName(varName.name());
capturedField =
ParentCapture.create(params.type, name, parentParameterLookup.getLocal(varName));
syntheticFields.put(varName, capturedField);
}
return capturedField.field().accessor(thisVar);
}
Iterable<ParentCapture> getCapturedFields() {
return Iterables.concat(
Iterables.filter(asList(paramsCapture, ijCapture, renderContextCapture), notNull()),
paramFields.values(),
localFields.values(),
syntheticFields.values());
}
@Override
public Expression getRenderContext() {
if (renderContextCapture == null) {
params.fieldNames.claimName(RENDER_CONTEXT_FIELD);
renderContextCapture =
ParentCapture.create(
params.type, RENDER_CONTEXT_FIELD, parentParameterLookup.getRenderContext());
}
return renderContextCapture.field().accessor(thisVar);
}
@Override
public Expression getParamsRecord() {
if (paramsCapture == null) {
params.fieldNames.claimName(PARAMS_FIELD);
paramsCapture =
ParentCapture.create(
params.type, PARAMS_FIELD, parentParameterLookup.getParamsRecord());
}
return paramsCapture.field().accessor(thisVar);
}
@Override
public Expression getIjRecord() {
if (ijCapture == null) {
params.fieldNames.claimName(IJ_FIELD);
ijCapture =
ParentCapture.create(params.type, IJ_FIELD, parentParameterLookup.getIjRecord());
}
return ijCapture.field().accessor(thisVar);
}
}
}