Partial Template Reduction (3)

At the end of the summer vacation, here is the last post on the topic of Partial Template Reduction for the time being. The basic algorithm was presented in the first two articles. In this article, the missing variable support is added.

The following example shows two directives that could not be reduced correctly until now.

<#var name=firstname>
<#list seq as s with l>
${l?counter}. ${company}/${s} ${name}
</#list>

The Var-Directive creates variables whose name is known within a defined scope. Outside of this scope, there is no longer any access to the content of the variable. The List-Directive defines the variable s and the loop variable l, which can be accessed within the directive.

Without special handling in the reduce method, all these variables can cause incorrect results. Within Partial Template Reduction, the variables of the List-Directive have no value and their interpolations are not replaced. If the variable of the Var-Directive has no value, it is not replaced either.

Variables and model data can overlap if they have the same name. With the previous reduce implementation, however, it is not possible to determine whether one variable overlaps another because the scopes were ignored.

When processing a template with the process method, validity ranges are created at necessary points using special Environment implementations. These Environment instances are chained and the search for a variable value is passed through from the topmost Environment to the BaseEnvironment. If the variable is found in one of the Environment instances, the search is aborted and the value is returned. If the value is not found in the BaseEnvironment, the search is terminated with an RuntimeException.

There is a special ListEnvironment for the List-Directive that contains the loop variables. When these variables are accessed within the loop, they can be found via the ListEnvironment. If another loop with identical variables exists within the loop, only variables from the inner loop are found within this loop. This happens because the inner loops ListEnvironment is searched first.

The Fragment implementations for the List-Directive therefore also require an Environment in their reduce method to define the scope of their variables.

public class SequenceListFragment extends AbstractListFragment<Object> {

  // ...

  @Override
  public Fragment reduce(ReduceContext context) {
    Environment environment = context.getEnvironment();
    try {
      context.setEnvironment(new ReducingVariableEnvironment(new ReducingLoopVariableEnvironment(environment, identifier, looperIdentifier)));
      return optimize(block, block.reduce(context), 
         r -> new SequenceListFragment(list, identifier, looperIdentifier, r, ftl));
    } finally {
      context.setEnvironment(environment);
    }
  }
}

In the new reduce method of the SequenceListFragment, a new ReducingVariableEnvironment and ReducingLoopVariableEnvironment are inserted at the beginning of the environment chain and then the reduction is carried out.

public class ReducingLoopVariableEnvironment extends WrapperEnvironment {
    private final List<String> identifiers;

    public ReducingLoopVariableEnvironment(Environment wrapped, String... identifiers) {
        super(wrapped);
        this.identifiers = Arrays.stream(identifiers).filter(Objects::nonNull).toList();
    }

    @Override
    public TemplateObject getValue(String name) {
        return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getValue(name);
    }
}

The ReducingLoopVariableEnvironment is used because the loop variables never have a value during the reduction, only their existence is checked. If an outer variable with a value exists, it is successfully covered during the reduction and the value of the outer variable is not inadvertently inserted.

The reduction in the VariableFragment is somewhat more complex because various cases have to be taken into account. First of all, the value of the variable can be NULL during the reduction because it cannot be found in the model. In this case, the VariableFragment remains unchanged and is returned directly. In the other case, the variable is created or changed in the Environment and a new VariableFragment is created

public class VariableFragment implements Fragment {

  // ...
 
  @Override
  public Fragment reduce(ReduceContext context) {
    Environment environment = context.getEnvironment();
    try {
      TemplateObject value = expression.evaluateToObject(context);
      if (value.isNull()) {
        return this;
      }
      if (exists) {
        if (environment.getVariable(name) == null) {
          return this;
        }
        environment.setVariable(name, value);
      } else {
        if (environment.checkVariable(name)) {
          return this;
        }
        environment.createVariable(name, value);
      }
      context.getStatus().changed().incrementAndGet();
      return new VariableFragment(name, value, exists, node);
    } catch (RuntimeException e) {
      return this;
    }
  }
}

This is where an optimization of a TemplateObject expression comes into play for the first time. Because if the value of the variable has already been calculated, why shouldn’t it be used directly in the VariableFragment?

For this to work, the ReducingVariableEnvironment must be included in the reduce method where a VariableEnvironment is required in the process method.

public class ReducingVariableEnvironment extends WrapperEnvironment {
  private final Set<String> identifiers = new HashSet<>();

  public ReducingVariableEnvironment(Environment wrapped) {
    super(wrapped);
  }

  @Override
  public TemplateObject getValue(String name) {
    return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getValue(name);
  }

  @Override
  public void createVariable(String name, TemplateObject value) {
    identifiers.add(name);
  }

  @Override
  public void setVariable(String name, TemplateObject value) {
    if (!identifiers.contains(name)) {
      wrapped.setVariable(name, value);
      return;
    }
    identifiers.add(name);
  }

  @Override
  public boolean checkVariable(String name) {
    return identifiers.contains(name);
  }

  @Override
  public TemplateObject getVariable(String name) {
    return identifiers.contains(name) ? TemplateNull.NULL : wrapped.getVariable(name);
  }
}

This Environment returns a NULL value for variables that were created with the Var- or Set-Directive, so that there are no evaluations that refer to variables. A variable that only refers to expressions with constants or model values can be optimized, all others cannot.

This completes the first implementation of the Partial Template Reduction feature and creates a real USP for FreshMarker. As always, the new version can be found on Maven Central.

Leave a Comment