Partial Template Reduktion (2)

In the first article on the implementation of the Partial Template Reduction feature, a few basic functions were created. This part deals with the implementation for the If-, Switch- and List- Directives.

If- and Switch-Directives both have a very similar structure. The IfFragment and the SwitchFragment each contain a list fragments of ConditionalFragment and an optional Fragment named endFragment. The list corresponds to the Case blocks of the Switch-Directive and the If and ElseIf blocks in the If-Directive. The optional Fragment corresponds to the Default and the Else block. When evaluating the conditional directives, their internal list of ConditionalFragment instances is run through until one of them match the condition.

If a matching ConditionalFragment is found, the overall processing is continued with its internal Fragment. If no suitable ConditionalFragment is found, the processing may continue with the endFragment.

A positive match of a ConditionalFragment means that true is returned when the expression is evaluated. This means for the ConditionalFragment instances in the IfFragment, the expressions in the filterByConditional method are compared with TemplateBoolean.TRUE. If the comparison is successful, the corresponding fragment is reduced, otherwise the endFragment is reduced.

public class IfFragment implements Fragment {

  // ..

  @Override
  public Fragment reduce(ReduceContext context) {
    try {
      for (ConditionalFragment fragment : fragments) {
        if (filterByConditional(context, fragment)) {
          return fragment.reduce(context);
        }
      }
      return endFragment.reduce(context);
    } catch (RuntimeException e) {
      log.info("cannot reduce: {}", e.getMessage(), e);
    }
    return new IfFragment(fragments.stream().map(f -> f.reduce(context)).toList(), endFragment.reduce(context));
  }
}

For the ConditionalFragment instances in the SwitchFragment, the expression in the Switch-Directive is evaluated first and then compared with the expressions of the individual Cases. In fact, complex expressions and not just constants can be used in a Case in FreshMarker. In the future, there may be an optimization for constant cases.

public class SwitchFragment implements Fragment {
 
  // ...

  @Override
  public Fragment reduce(ReduceContext context) {
    try {
      TemplatePrimitive<?> switchValue = evaluatePrimitive(this.switchExpression, context, node);
      for (ConditionalFragment fragment : fragments) {
        if (switchValue.equals(evaluatePrimitive(fragment.getConditional(), context, fragment.getNode()))) {
          return fragment.reduce(context);
        }
      }
      return defaultFragment.reduce(context);
    } catch (RuntimeException e) {
      log.info("cannot reduce: {}", e.getMessage(), e);
    }
    SwitchFragment switchFragment = new SwitchFragment(switchExpression, node);
    switchFragment.fragments.addAll(fragments.stream().map(f -> f.reduce(context)).toList());
    switchFragment.defaultFragment = defaultFragment.reduce(context);
    return switchFragment;
  }
}

The same applies to the SwitchFragment. If the comparison is successful, the corresponding Fragment is reduced, otherwise the endFragment is reduced. Both Fragment implementations have the getSize implementation in common. The size is defined as the sum of the sizes of the sub-fragments plus 1.

@Override
public int getSize() {
  return fragments.stream().mapToInt(Fragment::getSize).sum() + endFragment.getSize() + 1;
}

The ConditionalFragment instances contain an expression conditional and a Fragment named content.

public class ConditionalFragment implements Fragment {

  private final TemplateObject conditional;
  private final Fragment content;
  
  // ...

  @Override
  public ConditionalFragment reduce(ReduceContext context) {
    try {
      BlockFragment reduce = content.reduce(context);
      if (reduce == content) {
        return this;
      }
      context.getStatus().changed().incrementAndGet();
      return new ConditionalFragment(conditional, reduce, node);
    } catch (RuntimeException e) {
      return this;
    }
  }

  @Override
  public int getSize() {
    return content.getSize() + 1;
  }
}

The reduce method reduces the content and returns a new ConditionalFragment if the content has actually changed. In the other case, the current ConditionalFragment is simply returned.

At this point there is another possibility to reduce the templates. The expressions in the ConditionalFragments could actually also be reduced if the expressions could be simplified. However, this is reserved for a later version of Partial Template Reduction.

One last interesting Directive to support is the List-Directive. Its two implementations for Hashes and Sequences can be found in the corresponding Fragment classes HashListFragment and SequenceListFragment. They contain a Fragment for the block that is to be run through multiple times.

@Override
public SequenceListFragment reduce(ReduceContext context) {
  return optimize(block, block.reduce(context), 
    r -> new SequenceListFragment(list, identifier, looperIdentifier, r, ftl))
}

@Override
public HashListFragment reduce(ReduceContext context) {
  return optimize(block, block.reduce(context), 
    r -> new HashListFragment(list, keyIdentifier, valueIdentifier, looperIdentifier, r, ftl, comparator));
}

To reduce, the internal block is reduced for both implementations and its result is evaluated in the optimize method. The optimize method looks at three cases. If the result of the reduction is ConstantFragment.EMPTY, then the list has no body and the entire list is replaced by ConstantFragment.EMPTY. If the reduction returns the identical object, then the list is unchanged and returned as usual. Otherwise, a new Fragment is created.

This implementation therefore already specifies the restriction of the reduce method for the List-Directive. As the evaluation is only carried out with the variables from the partial model, there are no loop variables that can be used for reduction. Only the invariants in the list can be reduced. Variables within the list should also not hide model variables, otherwise the wrong values will be reduced. However, this shortcomings will be corrected in the next version of the feature.

Apart from one small crucial detail, all the important details for the Partial Template Reduction feature have now been implemented. There were a few more fragments to adapt, but their changes are boring. The following example shows the possibilities of the new feature so far.

<#switch flag>
<#case 1>${company}
<#case 2>${name!'Jens'}
<#case 3><#if name??>${name}<#else>Anonymous</#if>
<#default>default
</#switch>

For the reduction with template.reduce(Map.of("flag", 2) or template.reduce(Map.of("flag", 3), we expect the result to be a Template that generates Regina as the output when processing with the value Regina for name.

With the previous changes, however, we get the output Jens for 2 and the output Anonymous for 3. Here, the Default– and Exist-Operators do not yet work correctly with the reduce method. Both operators do not find the value for name and therefore evaluate the expression incorrectly. This results in an undesired reduction. To prevent this reduction, the evaluateToObject methods must be informed that it is a reduce call and not a process call.

@Override
public TemplateObject evaluateToObject(ProcessContext context) {
    TemplateObject templateObject = expression.evaluateToObject(context);
    return context.reductionCheck(templateObject) ? TemplateBoolean.TRUE : TemplateBoolean.FALSE;
}

The ProcessContext class has an implementation of reductionCheck that returns true if the expression is not evaluated to a TemplateNull value. This method is overwritten in the ReduceContext so that a ReduceException is thrown for a TemplateNull value. This cancels the reduction of the corresponding fragment.

In the example shown above, 12 of the original 22 Fragment instances are removed. The whole SwitchFragment is replaced by the ConditinalFragment for case 3.

The Partial Template Reduction feature can be used with the latest version of FreshMarker on Maven Central.

1 thought on “Partial Template Reduktion (2)”

Leave a Comment