Java tutorial
/* * 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.Preconditions.checkNotNull; import static com.google.template.soy.jbcsrc.BytecodeUtils.COMPILED_TEMPLATE_TYPE; import static com.google.template.soy.jbcsrc.BytecodeUtils.CONTENT_KIND_TYPE; import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_VALUE_PROVIDER_TYPE; import static com.google.template.soy.jbcsrc.BytecodeUtils.compareSoyEquals; import static com.google.template.soy.jbcsrc.BytecodeUtils.constant; import static com.google.template.soy.jbcsrc.BytecodeUtils.constantNull; import static com.google.template.soy.jbcsrc.Statement.NULL_STATEMENT; import static com.google.template.soy.jbcsrc.VariableSet.SaveStrategy.DERIVED; import static com.google.template.soy.jbcsrc.VariableSet.SaveStrategy.STORE; import com.google.auto.value.AutoValue; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.internal.ParamStore; import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.jbcsrc.ControlFlow.IfBlock; import com.google.template.soy.jbcsrc.ExpressionCompiler.BasicExpressionCompiler; import com.google.template.soy.jbcsrc.MsgCompiler.SoyNodeToStringCompiler; import com.google.template.soy.jbcsrc.VariableSet.SaveStrategy; import com.google.template.soy.jbcsrc.VariableSet.Scope; import com.google.template.soy.jbcsrc.VariableSet.Variable; import com.google.template.soy.jbcsrc.shared.RenderContext; import com.google.template.soy.msgs.internal.MsgUtils; import com.google.template.soy.msgs.internal.MsgUtils.MsgPartsAndIds; import com.google.template.soy.soytree.AbstractReturningSoyNodeVisitor; import com.google.template.soy.soytree.CallBasicNode; import com.google.template.soy.soytree.CallDelegateNode; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallNode.DataAttribute; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CallParamNode; import com.google.template.soy.soytree.CallParamValueNode; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.DebuggerNode; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForNode.RangeArgs; import com.google.template.soy.soytree.ForeachIfemptyNode; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.ForeachNonemptyNode; import com.google.template.soy.soytree.IfCondNode; import com.google.template.soy.soytree.IfElseNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; import com.google.template.soy.soytree.LetValueNode; import com.google.template.soy.soytree.LogNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.PrintDirectiveNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.BlockNode; import com.google.template.soy.soytree.SoyNode.RenderUnitNode; import com.google.template.soy.soytree.SwitchCaseNode; import com.google.template.soy.soytree.SwitchDefaultNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.XidNode; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; /** * Compiles {@link SoyNode soy nodes} into {@link Statement statements}. * * <p>The normal contract for {@link Statement statements} is that they leave the state of the * runtime stack unchanged before and after execution. The SoyNodeCompiler requires that the * runtime stack be <em>empty</em> prior to any of the code produced. */ final class SoyNodeCompiler extends AbstractReturningSoyNodeVisitor<Statement> { // TODO(lukes): consider introducing a Builder or a non-static Factory. /** * Creates a SoyNodeCompiler * * @param innerClasses The current set of inner classes * @param stateField The field on the current class that holds the state variable * @param thisVar An expression that returns 'this' * @param appendableVar An expression that returns the current AdvisingAppendable that we are * rendering into * @param variableSet The variable set for generating locals and fields * @param variables The variable lookup table for reading locals. */ static SoyNodeCompiler create(CompiledTemplateRegistry registry, InnerClasses innerClasses, FieldRef stateField, Expression thisVar, AppendableExpression appendableVar, VariableSet variableSet, VariableLookup variables) { DetachState detachState = new DetachState(variableSet, thisVar, stateField); ExpressionCompiler expressionCompiler = ExpressionCompiler.create(detachState, variables); ExpressionToSoyValueProviderCompiler soyValueProviderCompiler = ExpressionToSoyValueProviderCompiler .create(expressionCompiler, variables); return new SoyNodeCompiler(thisVar, registry, detachState, variableSet, variables, appendableVar, expressionCompiler, soyValueProviderCompiler, new LazyClosureCompiler(registry, innerClasses, variables, soyValueProviderCompiler)); } private final Expression thisVar; private final CompiledTemplateRegistry registry; private final DetachState detachState; private final VariableSet variables; private final VariableLookup variableLookup; private final AppendableExpression appendableExpression; private final ExpressionCompiler exprCompiler; private final ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler; private final LazyClosureCompiler lazyClosureCompiler; private Scope currentScope; SoyNodeCompiler(Expression thisVar, CompiledTemplateRegistry registry, DetachState detachState, VariableSet variables, VariableLookup variableLookup, AppendableExpression appendableExpression, ExpressionCompiler exprCompiler, ExpressionToSoyValueProviderCompiler expressionToSoyValueProviderCompiler, LazyClosureCompiler lazyClosureCompiler) { this.thisVar = checkNotNull(thisVar); this.registry = checkNotNull(registry); this.detachState = checkNotNull(detachState); this.variables = checkNotNull(variables); this.variableLookup = checkNotNull(variableLookup); this.appendableExpression = checkNotNull(appendableExpression); this.exprCompiler = checkNotNull(exprCompiler); this.expressionToSoyValueProviderCompiler = checkNotNull(expressionToSoyValueProviderCompiler); this.lazyClosureCompiler = checkNotNull(lazyClosureCompiler); } @AutoValue abstract static class CompiledMethodBody { static CompiledMethodBody create(Statement body, int numDetaches) { return new AutoValue_SoyNodeCompiler_CompiledMethodBody(body, numDetaches); } abstract Statement body(); abstract int numberOfDetachStates(); } CompiledMethodBody compile(TemplateNode node) { Statement templateBody = visit(node); return getCompiledBody(templateBody); } CompiledMethodBody compileChildren(RenderUnitNode node) { Statement templateBody = visitChildrenInNewScope(node); return getCompiledBody(templateBody); } private CompiledMethodBody getCompiledBody(Statement templateBody) { Statement jumpTable = detachState.generateReattachTable(); return CompiledMethodBody.create(Statement.concat(jumpTable, templateBody), detachState.getNumberOfDetaches()); } @Override protected Statement visit(SoyNode node) { try { return super.visit(node); } catch (UnexpectedCompilerFailureException e) { e.addLocation(node.getSourceLocation()); throw e; } catch (Throwable t) { throw new UnexpectedCompilerFailureException(node.getSourceLocation(), t); } } @Override protected Statement visitTemplateNode(TemplateNode node) { return visitChildrenInNewScope(node); } private Statement visitChildrenInNewScope(BlockNode node) { Scope prev = currentScope; currentScope = variables.enterScope(); List<Statement> children = visitChildren(node); Statement leave = currentScope.exitScope(); children.add(leave); currentScope = prev; return Statement.concat(children).withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitIfNode(IfNode node) { List<IfBlock> ifs = new ArrayList<>(); Optional<Statement> elseBlock = Optional.absent(); for (SoyNode child : node.getChildren()) { if (child instanceof IfCondNode) { IfCondNode icn = (IfCondNode) child; SoyExpression cond = exprCompiler.compile(icn.getExprUnion().getExpr()).coerceToBoolean(); Statement block = visitChildrenInNewScope(icn); ifs.add(IfBlock.create(cond, block)); } else { IfElseNode ien = (IfElseNode) child; elseBlock = Optional.of(visitChildrenInNewScope(ien)); } } return ControlFlow.ifElseChain(ifs, elseBlock).withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitSwitchNode(SwitchNode node) { // A few special cases: // 1. only a {default} block. In this case we can skip all the switch logic and temporaries // 2. no children. Just return the empty statement // Note that in both of these cases we do not evalutate (or generate code) for the switch // expression. List<SoyNode> children = node.getChildren(); if (children.isEmpty()) { return Statement.NULL_STATEMENT; } if (children.size() == 1 && children.get(0) instanceof SwitchDefaultNode) { return visitChildrenInNewScope((SwitchDefaultNode) children.get(0)); } // otherwise we need to evaluate the predicate and generate dispatching logic. SoyExpression expression = exprCompiler.compile(node.getExpr()); Statement init; List<IfBlock> cases = new ArrayList<>(); Optional<Statement> defaultBlock = Optional.absent(); Scope scope = variables.enterScope(); Variable variable = scope.createSynthetic(SyntheticVarName.forSwitch(node), expression, STORE); init = variable.initializer(); expression = expression.withSource(variable.local()); for (SoyNode child : children) { if (child instanceof SwitchCaseNode) { SwitchCaseNode caseNode = (SwitchCaseNode) child; Label reattachPoint = new Label(); List<Expression> comparisons = new ArrayList<>(); for (ExprRootNode caseExpr : caseNode.getExprList()) { comparisons.add(compareSoyEquals(expression, exprCompiler.compile(caseExpr, reattachPoint))); } Expression condition = BytecodeUtils.logicalOr(comparisons).labelStart(reattachPoint); Statement block = visitChildrenInNewScope(caseNode); cases.add(IfBlock.create(condition, block)); } else { SwitchDefaultNode defaultNode = (SwitchDefaultNode) child; defaultBlock = Optional.of(visitChildrenInNewScope(defaultNode)); } } Statement exitScope = scope.exitScope(); // Soy allows arbitrary expressions to appear in {case} statements within a {switch}. // Java/C, by contrast, only allow some constant expressions in cases. // TODO(lukes): in practice the case statements are often constant strings/ints. If everything // is typed to int/string we should consider implementing via the tableswitch/lookupswitch // instruction which would be way way way faster. cglib has some helpers for string switch // generation that we could maybe use return Statement.concat(init, ControlFlow.ifElseChain(cases, defaultBlock), exitScope) .withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitForNode(ForNode node) { // Despite appearances, range() is not a soy function, it is essentially a keyword that only // works in for loops, there are 3 forms. // {for $i in range(3)}{$i}{/for} -> 0 1 2 // {for $i in range(2, 5)} ... {/for} -> 2 3 4 // {for $i in range(2, 8, 2)} ... {/for} -> 2 4 6 Scope scope = variables.enterScope(); final CompiledRangeArgs rangeArgs = calculateRangeArgs(node, scope); final Statement loopBody = visitChildrenInNewScope(node); // Note it is important that exitScope is called _after_ the children are visited. // TODO(lukes): this is somewhat error-prone... we could maybe manage it by have the scope // maintain a sequence of statements and then all statements would be added to Scope which would // return a statement for the whole thing at the end... would that be clearer? final Statement exitScope = scope.exitScope(); return new Statement(node.getSourceLocation()) { @Override void doGen(CodeBuilder adapter) { for (Statement initializer : rangeArgs.initStatements()) { initializer.gen(adapter); } // We need to check for an empty loop by doing an entry test Label loopStart = adapter.mark(); // If current >= limit we are done rangeArgs.currentIndex().gen(adapter); rangeArgs.limit().gen(adapter); Label end = new Label(); adapter.ifCmp(Type.INT_TYPE, Opcodes.IFGE, end); loopBody.gen(adapter); // at the end of the loop we need to increment and jump back. rangeArgs.increment().gen(adapter); adapter.goTo(loopStart); adapter.mark(end); exitScope.gen(adapter); } }; } @AutoValue abstract static class CompiledRangeArgs { /** Current loop index. */ abstract Expression currentIndex(); /** Where to end loop iteration, defaults to {@code 0}. */ abstract Expression limit(); /** This statement will increment the index by the loop stride. */ abstract Statement increment(); /** Statements that must have been run prior to using any of the above expressions. */ abstract ImmutableList<Statement> initStatements(); } /** * Interprets the given expressions as the arguments of a {@code range(...)} expression in a * {@code for} loop. */ private CompiledRangeArgs calculateRangeArgs(ForNode forNode, Scope scope) { RangeArgs rangeArgs = forNode.getRangeArgs(); final ImmutableList.Builder<Statement> initStatements = ImmutableList.builder(); final Variable currentIndex; if (rangeArgs.start().isPresent()) { Label startDetachPoint = new Label(); Expression startIndex = MethodRef.INTS_CHECKED_CAST .invoke(exprCompiler.compile(rangeArgs.start().get(), startDetachPoint).unboxAs(long.class)); currentIndex = scope.create(forNode.getVarName(), startIndex, STORE); initStatements.add(currentIndex.initializer().labelStart(startDetachPoint)); } else { currentIndex = scope.create(forNode.getVarName(), constant(0), STORE); initStatements.add(currentIndex.initializer()); } final Statement incrementCurrentIndex; if (rangeArgs.increment().isPresent()) { Label detachPoint = new Label(); Expression increment = MethodRef.INTS_CHECKED_CAST .invoke(exprCompiler.compile(rangeArgs.increment().get(), detachPoint).unboxAs(long.class)); // If the expression is non-trivial, make sure to save it to a field. final Variable incrementVariable = scope.createSynthetic(SyntheticVarName.forLoopIncrement(forNode), increment, increment.isCheap() ? DERIVED : STORE); initStatements.add(incrementVariable.initializer().labelStart(detachPoint)); incrementVariable.local(); incrementCurrentIndex = new Statement() { @Override void doGen(CodeBuilder adapter) { currentIndex.local().gen(adapter); incrementVariable.local().gen(adapter); adapter.visitInsn(Opcodes.IADD); adapter.visitVarInsn(Opcodes.ISTORE, currentIndex.local().index()); } }; } else { incrementCurrentIndex = new Statement() { @Override void doGen(CodeBuilder adapter) { adapter.iinc(currentIndex.local().index(), 1); } }; } Label detachPoint = new Label(); Expression limit = MethodRef.INTS_CHECKED_CAST .invoke(exprCompiler.compile(rangeArgs.limit(), detachPoint).unboxAs(long.class)); // If the expression is non-trivial we should cache it in a local variable Variable variable = scope.createSynthetic(SyntheticVarName.forLoopLimit(forNode), limit, limit.isCheap() ? DERIVED : STORE); initStatements.add(variable.initializer().labelStart(detachPoint)); limit = variable.local(); return new AutoValue_SoyNodeCompiler_CompiledRangeArgs(currentIndex.local(), limit, incrementCurrentIndex, initStatements.build()); } @Override protected Statement visitForeachNode(ForeachNode node) { ForeachNonemptyNode nonEmptyNode = (ForeachNonemptyNode) node.getChild(0); SoyExpression expr = exprCompiler.compile(node.getExpr()).unboxAs(List.class); Scope scope = variables.enterScope(); final Variable listVar = scope.createSynthetic(SyntheticVarName.foreachLoopList(nonEmptyNode), expr, STORE); final Variable indexVar = scope.createSynthetic(SyntheticVarName.foreachLoopIndex(nonEmptyNode), constant(0), STORE); final Variable listSizeVar = scope.createSynthetic(SyntheticVarName.foreachLoopLength(nonEmptyNode), MethodRef.LIST_SIZE.invoke(listVar.local()), DERIVED); final Variable itemVar = scope.create(nonEmptyNode.getVarName(), MethodRef.LIST_GET.invoke(listVar.local(), indexVar.local()).cast(SOY_VALUE_PROVIDER_TYPE), SaveStrategy.DERIVED); final Statement loopBody = visitChildrenInNewScope(nonEmptyNode); final Statement exitScope = scope.exitScope(); // it important for this to be generated after exitScope is called (or before enterScope) final Statement emptyBlock = node.numChildren() == 2 ? visitChildrenInNewScope((ForeachIfemptyNode) node.getChild(1)) : null; return new Statement() { @Override void doGen(CodeBuilder adapter) { listVar.initializer().gen(adapter); listSizeVar.initializer().gen(adapter); listSizeVar.local().gen(adapter); Label emptyListLabel = new Label(); adapter.ifZCmp(Opcodes.IFEQ, emptyListLabel); indexVar.initializer().gen(adapter); Label loopStart = adapter.mark(); itemVar.initializer().gen(adapter); loopBody.gen(adapter); adapter.iinc(indexVar.local().index(), 1); // index++ indexVar.local().gen(adapter); listSizeVar.local().gen(adapter); adapter.ifICmp(Opcodes.IFLT, loopStart); // if index < list.size(), goto loopstart // exit the loop exitScope.gen(adapter); if (emptyBlock != null) { Label skipIfEmptyBlock = new Label(); adapter.goTo(skipIfEmptyBlock); adapter.mark(emptyListLabel); emptyBlock.gen(adapter); adapter.mark(skipIfEmptyBlock); } else { adapter.mark(emptyListLabel); } } }; } @Override protected Statement visitPrintNode(PrintNode node) { // First check our special case for compatible content types (no print directives) and an // expression that evaluates to a SoyValueProvider. This will allow us to render incrementally if (node.getChildren().isEmpty()) { Label reattachPoint = new Label(); ExprRootNode expr = node.getExprUnion().getExpr(); Optional<Expression> asSoyValueProvider = expressionToSoyValueProviderCompiler .compileAvoidingBoxing(expr, reattachPoint); if (asSoyValueProvider.isPresent()) { return renderIncrementally(node, asSoyValueProvider.get(), reattachPoint); } } // otherwise we need to do some escapes or simply cannot do incremental rendering Label reattachPoint = new Label(); SoyExpression value = compilePrintNodeAsExpression(node, reattachPoint); AppendableExpression renderSoyValue = appendableExpression.appendString(value.coerceToString()) .labelStart(reattachPoint); return detachState.detachLimited(renderSoyValue).withSourceLocation(node.getSourceLocation()); } private SoyExpression compilePrintNodeAsExpression(PrintNode node, Label reattachPoint) { BasicExpressionCompiler basic = exprCompiler.asBasicCompiler(reattachPoint); SoyExpression value = basic.compile(node.getExprUnion().getExpr()); // We may have print directives, that means we need to pass the render value through a bunch of // SoyJavaPrintDirective.apply methods. This means lots and lots of boxing. // TODO(user): tracks adding streaming print directives which would help with this, // because instead of wrapping the soy value, we would just wrap the appendable. for (PrintDirectiveNode printDirective : node.getChildren()) { value = value.applyPrintDirective(variableLookup.getRenderContext(), printDirective.getName(), basic.compileToList(printDirective.getArgs())); } return value; } /** * TODO(lukes): if the expression is a param, then this is kind of silly since it looks like * <pre>{@code * SoyValueProvider localParam = this.param; * this.currentRenderee = localParam; * SoyValueProvider localRenderee = this.currentRenderee; * localRenderee.renderAndResolve(); * }</pre> * * <p>In this case we could elide the currentRenderee altogether if we knew the soyValueProvider * expression was just a field read... And this is the _common_case for .renderAndResolve calls. * to actually do this we could add a mechanism similar to the SaveStrategy enum for expressions, * kind of like {@link Expression#isCheap()} which isn't that useful in practice. */ private Statement renderIncrementally(PrintNode node, Expression soyValueProvider, Label reattachPoint) { // In this case we want to render the SoyValueProvider via renderAndResolve which will // enable incremental rendering of parameters for lazy transclusions! // This actually ends up looking a lot like how calls work so we use the same strategy. FieldRef currentRendereeField = variables.getCurrentRenderee(); Statement initRenderee = currentRendereeField.putInstanceField(thisVar, soyValueProvider) .labelStart(reattachPoint); // This cast will always succeed. Expression callRenderAndResolve = currentRendereeField.accessor(thisVar).invoke( MethodRef.SOY_VALUE_PROVIDER_RENDER_AND_RESOLVE, appendableExpression, // the isLast param // TODO(lukes): pass a real value here when we have expression use analysis. constant(false)); Statement doCall = detachState.detachForRender(callRenderAndResolve); Statement clearRenderee = currentRendereeField.putInstanceField(thisVar, constantNull(SOY_VALUE_PROVIDER_TYPE)); return Statement.concat(initRenderee, doCall, clearRenderee).withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitRawTextNode(RawTextNode node) { AppendableExpression render = appendableExpression.appendString(constant(node.getRawText())); // TODO(lukes): add some heuristics about when to add this // ideas: // * never try to detach in certain 'contexts' (e.g. attribute context) // * never detach after rendering small chunks (< 128 bytes?) return detachState.detachLimited(render).withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitDebuggerNode(DebuggerNode node) { // intentional no-op. java has no 'breakpoint' equivalent. But we can add a label + line // number. Which may be useful for debugging :) return NULL_STATEMENT.withSourceLocation(node.getSourceLocation()); } // Note: xid and css translations are expected to be very short, so we do _not_ generate detaches // for them, even though they write to the output. @Override protected Statement visitXidNode(XidNode node) { return appendableExpression.appendString(variableLookup.getRenderContext() .invoke(MethodRef.RENDER_CONTEXT_RENAME_XID, constant(node.getText()))).toStatement() .withSourceLocation(node.getSourceLocation()); } // TODO(lukes): The RenderVisitor optimizes css/xid renaming by stashing a one element cache in // the CSS node itself (keyed off the identity of the renaming map). We could easily add such // an optimization via a static field in the Template class. Though im not sure it makes sense // as an optimization... this should just be an immutable map lookup keyed off of a constant // string. If we cared a lot, we could employ a simpler (and more compact) optimization by // assigning each selector a unique integer id and then instead of hashing we can just reference // an array (aka perfect hashing). This could be part of our runtime library and ids could be // assigned at startup. @Override protected Statement visitCssNode(CssNode node) { Expression renamedSelector = variableLookup.getRenderContext() .invoke(MethodRef.RENDER_CONTEXT_RENAME_CSS_SELECTOR, constant(node.getSelectorText())); if (node.getComponentNameExpr() != null) { Label reattachPoint = new Label(); SoyExpression compiledComponent = exprCompiler.compile(node.getComponentNameExpr(), reattachPoint) .coerceToString(); return appendableExpression.appendString(compiledComponent).appendChar(constant('-')) .appendString(renamedSelector).labelStart(reattachPoint).toStatement() .withSourceLocation(node.getSourceLocation()); } return appendableExpression.appendString(renamedSelector).toStatement() .withSourceLocation(node.getSourceLocation()); } /** * MsgFallbackGroupNodes have either one or two children. In the 2 child case the second child is * the {@code {fallbackmsg}} entry. For this we generate code that looks like: * <pre> {@code * if (renderContext.hasMsg(primaryId)) { * <render primary msg> * } else { * <render fallback msg> * } * }</pre> * * <p>All of the logic for actually rendering {@code msg} nodes is handled by the * {@link MsgCompiler}. */ @Override protected Statement visitMsgFallbackGroupNode(MsgFallbackGroupNode node) { MsgNode msg = node.getMsg(); MsgPartsAndIds idAndParts = MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(msg); ImmutableList<String> escapingDirectives = node.getEscapingDirectiveNames(); Statement renderDefault = getMsgCompiler().compileMessage(idAndParts, msg, escapingDirectives); // fallback groups have 1 or 2 children. if there are 2 then the second is a fallback and we // need to check for presence. if (node.hasFallbackMsg()) { MsgNode fallback = node.getFallbackMsg(); MsgPartsAndIds fallbackIdAndParts = MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(fallback); IfBlock ifAvailableRenderDefault = IfBlock .create(variableLookup.getRenderContext().invoke(MethodRef.RENDER_CONTEXT_USE_PRIMARY_MSG, constant(idAndParts.id), constant(fallbackIdAndParts.id)), renderDefault); return ControlFlow.ifElseChain(ImmutableList.of(ifAvailableRenderDefault), Optional.of(getMsgCompiler().compileMessage(fallbackIdAndParts, fallback, escapingDirectives))); } else { return renderDefault; } } /** * Given this delcall: * {@code {delcall foo.bar variant="$expr" allowemptydefault="true"}} * * Generate code that looks like: * <pre> {@code * renderContext.getDeltemplate("foo.bar", <variant-expression>, true) * .create(<prepareParameters>, ijParams) * .render(appendable, renderContext) * * }</pre> * * <p>We share logic with {@link #visitCallBasicNode(CallBasicNode)} around the actual calling * convention (setting up detaches, storing the template in a field). As well as the logic for * preparing the data record. The only interesting part of delcalls is calculating the * {@code variant} and the fact that we have to invoke the {@link RenderContext} runtime to do * the deltemplate lookup. */ @Override protected Statement visitCallDelegateNode(CallDelegateNode node) { Label reattachPoint = new Label(); Expression variantExpr; if (node.getDelCalleeVariantExpr() == null) { variantExpr = constant(""); } else { variantExpr = exprCompiler.compile(node.getDelCalleeVariantExpr(), reattachPoint).coerceToString(); } Expression calleeExpression = variableLookup.getRenderContext().invoke( MethodRef.RENDER_CONTEXT_GET_DELTEMPLATE, constant(node.getDelCalleeName()), variantExpr, constant(node.allowsEmptyDefault()), prepareParamsHelper(node, reattachPoint), variableLookup.getIjRecord()); return visitCallNodeHelper(node, registry.getDelTemplateContentKind(node.getDelCalleeName()), reattachPoint, calleeExpression); } @Override protected Statement visitCallBasicNode(CallBasicNode node) { // Basic nodes are basic! We can just call the node directly. CompiledTemplateMetadata callee = registry.getTemplateInfoByTemplateName(node.getCalleeName()); Label reattachPoint = new Label(); Expression calleeExpression = callee.constructor().construct(prepareParamsHelper(node, reattachPoint), variableLookup.getIjRecord()); return visitCallNodeHelper(node, callee.node().getContentKind(), reattachPoint, calleeExpression); } private Statement visitCallNodeHelper(CallNode node, @Nullable ContentKind calleeContentKind, Label reattachPoint, Expression calleeExpression) { FieldRef currentCalleeField = variables.getCurrentCalleeField(); // Apply escaping directives if any if (!node.getEscapingDirectiveNames().isEmpty()) { List<Expression> directiveExprs = new ArrayList<>(node.getEscapingDirectiveNames().size()); for (String directive : node.getEscapingDirectiveNames()) { directiveExprs.add(variableLookup.getRenderContext() .invoke(MethodRef.RENDER_CONTEXT_GET_PRINT_DIRECTIVE, constant(directive))); } calleeExpression = MethodRef.RUNTIME_APPLY_ESCAPERS.invoke(calleeExpression, BytecodeUtils.asList(directiveExprs), calleeContentKind == null ? BytecodeUtils.constantNull(CONTENT_KIND_TYPE) : FieldRef.enumReference(calleeContentKind).accessor()); } Statement initCallee = currentCalleeField.putInstanceField(thisVar, calleeExpression) .labelStart(reattachPoint); Expression callRender = currentCalleeField.accessor(thisVar).invoke(MethodRef.COMPILED_TEMPLATE_RENDER, appendableExpression, variableLookup.getRenderContext()); Statement callCallee = detachState.detachForRender(callRender); Statement clearCallee = currentCalleeField.putInstanceField(thisVar, BytecodeUtils.constantNull(COMPILED_TEMPLATE_TYPE)); return Statement.concat(initCallee, callCallee, clearCallee).withSourceLocation(node.getSourceLocation()); } private Expression prepareParamsHelper(CallNode node, Label reattachPoint) { DataAttribute dataAttribute = node.dataAttribute(); if (node.numChildren() == 0) { // Easy, just use the data attribute return getDataExpression(dataAttribute, reattachPoint); } else { // Otherwise we need to build a dictionary from {param} statements. Expression paramStoreExpression = getParamStoreExpression(node, dataAttribute, reattachPoint); for (CallParamNode child : node.getChildren()) { String paramKey = child.getKey(); Expression valueExpr; if (child instanceof CallParamContentNode) { valueExpr = lazyClosureCompiler.compileLazyContent("param", (CallParamContentNode) child, paramKey); } else { valueExpr = lazyClosureCompiler.compileLazyExpression("param", child, paramKey, ((CallParamValueNode) child).getValueExprUnion().getExpr()); } // ParamStore.setField return 'this' so we can just chain the invocations together. paramStoreExpression = MethodRef.PARAM_STORE_SET_FIELD.invoke(paramStoreExpression, BytecodeUtils.constant(paramKey), valueExpr); } return paramStoreExpression; } } /** * Returns an expression that creates a new {@link ParamStore} suitable for holding all the */ private Expression getParamStoreExpression(CallNode node, DataAttribute dataAttribute, Label reattachPoint) { Expression paramStoreExpression; if (dataAttribute.isPassingData()) { paramStoreExpression = ConstructorRef.AUGMENTED_PARAM_STORE .construct(getDataExpression(dataAttribute, reattachPoint), constant(node.numChildren())); } else { paramStoreExpression = ConstructorRef.BASIC_PARAM_STORE.construct(constant(node.numChildren())); } return paramStoreExpression; } private Expression getDataExpression(DataAttribute dataAttribute, Label reattachPoint) { if (dataAttribute.isPassingData()) { if (dataAttribute.isPassingAllData()) { return variableLookup.getParamsRecord(); } else { return exprCompiler.compile(dataAttribute.dataExpr(), reattachPoint).box().cast(SoyRecord.class); } } else { return FieldRef.EMPTY_DICT.accessor(); } } @Override protected Statement visitLogNode(LogNode node) { return compilerWithNewAppendable(AppendableExpression.logger()).visitChildrenInNewScope(node); } @Override protected Statement visitLetValueNode(LetValueNode node) { Expression newLetValue = lazyClosureCompiler.compileLazyExpression("let", node, node.getVarName(), node.getValueExpr()); return currentScope.create(node.getVarName(), newLetValue, STORE).initializer(); } @Override protected Statement visitLetContentNode(LetContentNode node) { Expression newLetValue = lazyClosureCompiler.compileLazyContent("let", node, node.getVarName()); return currentScope.create(node.getVarName(), newLetValue, STORE).initializer(); } @Override protected Statement visitMsgHtmlTagNode(MsgHtmlTagNode node) { // trivial node that is just a number of children surrounded by raw text nodes. return Statement.concat(visitChildren(node)).withSourceLocation(node.getSourceLocation()); } @Override protected Statement visitSoyNode(SoyNode node) { throw new UnsupportedOperationException( "The jbcsrc backend doesn't support: " + node.getKind() + " nodes yet."); } private MsgCompiler getMsgCompiler() { return new MsgCompiler(thisVar, detachState, variables, variableLookup, appendableExpression, new SoyNodeToStringCompiler() { @Override public Statement compileToBuffer(MsgHtmlTagNode htmlTagNode, AppendableExpression appendable) { return compilerWithNewAppendable(appendable).visit(htmlTagNode); } @Override public Expression compileToString(PrintNode node, Label reattachPoint) { return compilePrintNodeAsExpression(node, reattachPoint).coerceToString(); } @Override public Statement compileToBuffer(CallNode call, AppendableExpression appendable) { // TODO(lukes): in the case that CallNode has to be escaped we will render all the bytes // into a buffer, box it into a soy value, escape it, then copy the bytes into this // buffer. Consider optimizing at least one of the buffer copies away. return compilerWithNewAppendable(appendable).visit(call); } @Override public Expression compileToString(ExprRootNode node, Label reattachPoint) { return exprCompiler.compile(node, reattachPoint).coerceToString(); } @Override public Expression compileToInt(ExprRootNode node, Label reattachPoint) { return exprCompiler.compile(node, reattachPoint).box().cast(IntegerData.class); } }); } /** Returns a {@link SoyNodeCompiler} identical to this one but with an alternate appendable. */ private SoyNodeCompiler compilerWithNewAppendable(AppendableExpression appendable) { return new SoyNodeCompiler(thisVar, registry, detachState, variables, variableLookup, appendable, exprCompiler, expressionToSoyValueProviderCompiler, lazyClosureCompiler); } }