Inside the Stage 1 FreshMarker Compiler

The first part of this series introduced the new FreshMarker compiler and compared it against the tree-walking interpreter. This second part zooms in on Stage 1, the untyped compiler, and shows exactly what code it emits, how the class is assembled.

Goal of Stage 1

Stage 1 has a modest but useful goal: eliminate interpretive fragment dispatch without asking the user for anything. There is no schema, no configuration change beyond Configuration(CompilerFeature.ENABLED), and the compiler still delegates every expression to the ordinary TemplateObject#evaluate(…​) machinery.

What actually moves out of interpretation is the control-flow skeleton of the template:

  • Constant text becomes a literal w.write("…​") call.
  • <#if><#list><#switch><#try> become real Java iffortry/catch statements.
  • Everything else becomes a call on a captured TemplateObject/Fragment field.

The JVM’s JIT sees a small, straight-line method with a stable shape, which it can inline aggressively.

The CompiledFragment contract

The runtime contract is a single method:

package org.freshmarker.core.compiler;

public interface CompiledFragment {
    void process(ProcessContext ctx);
}

Template#processCompiled(compiled, dataModel, writer) builds the same ProcessContext the interpreter would build and then invokes compiled.process(ctx). That means the compiled template shares the whole runtime infrastructure — formatters, output-format stack, locale, zone, TemplateSetting, extension registry — with an interpreted template.

The outer class is generated by FreshMarker itself

The scaffolding of the generated Java file is itself a FreshMarker template:

package ${packageName};
package ${packageName};

<#list imports as import>
import ${import};
</#list>
import org.freshmarker.core.ProcessException;

public class ${className} extends CompiledTemplate {

${fields}
  public ${className}(Object[] captured) {
    super(captured);
${ctorBody}
  }

  @Override
  @SuppressWarnings("unchecked")
  public void process(ProcessContext ctx) {
<#if hasTryCatch>
    try {
${body}
    } catch (IOException e) {
      throw new ProcessException(e.getMessage());
    }
<#else>
${body}</#if>
  }

${methods}
}

CompilerTemplates renders this file with the fragments produced by JavaSourceCodeGenerator. That is FreshMarker rendering its own compiler output — a small but very fitting piece of dogfooding.

Capturing: how the class carries interpreter state

AbstractJavaSourceCodeGenerator collects everything the generated code cannot express as raw Java into an Object[] captured array. Three helper methods create the fields and constructor assignments:

protected String captureExpression(TemplateObject obj) {   // -> expr_N
protected String captureFragment(Fragment obj) {           // -> frag_N
protected String captureMarkup(TemplateMarkup obj) {       // -> markup_N

Each helper appends a typed final field (private final TemplateObject expr_0;), an assignment in the constructor (this.expr_0 = (TemplateObject) captured[0];), and returns the field name for the caller to use in the emitted body. When the compiler is done, the GeneratedSource record wraps the source text and the corresponding Object[] capturedObjects array. Both are handed to FragmentCompiler, which uses javax.tools.JavaCompiler, an InMemoryFileManager and an InMemoryClassLoader to compile and load the class, and finally invokes it.

Of course, this approach has its limitations. The compiler will start complaining once the template class has 65,535 attributes or more.

What each fragment turns into

JavaSourceCodeGenerator implements TemplateVisitor<Void> and walks the fragment tree. Here is what each case looks like in the generated process(ctx) body.

Constant text

w.write("Hello World!");

Interpolation ${name}

The expression is captured and evaluated as a TemplateString:

w.write(markup_0.evaluateToObject(ctx).getValue());

If Directive

Real Java control flow, with the condition still evaluated through the interpreter:

if (expr_0.evaluate(ctx, TemplateBoolean.class) == TemplateBoolean.TRUE) {
  w.write("one");
} else if (expr_1.evaluate(ctx, TemplateBoolean.class) == TemplateBoolean.TRUE) {
  w.write("two");
} else {
  w.write("other");
}

List Directive

A for-loop over the evaluated sequence. The loop variable is pushed onto the environment (the environment stack is a shared runtime concept, and the interpreter uses the same variable name):

TemplateSequence<Object> seq_0 = expr_0.evaluate(ctx, TemplateSequence.class);
TemplateSequenceLooper looper_1 = new TemplateSequenceLooper(seq_0.sequence());
Environment oldEnv_3 = ctx.getEnvironment();
ListEnvironment env_2 = new ListEnvironment(oldEnv_3, "item", null, looper_1);
try {
  for (int i_4 = 0, n_4 = looper_1.size(); i_4 < n_4; i_4++) {
    ctx.setEnvironment(env_2);
    w.write(markup_2.evaluateToObject(ctx).getValue());
    w.write(" ");
    looper_1.increment();
  }
} finally {
  ctx.setEnvironment(oldEnv_3);
}

Hash-list iteration with plain <#list h as k, v> uses the same shape; combinations with sorted/filter/offset/limit/comparator currently fall back to a captured fragment.

Switch directive

Switch directive becomes an if/elseif chain over java.util.Objects.equals(…​)

Object switchVal_0 = expr_0.evaluateToObject(ctx);
if (java.util.Objects.equals(switchVal_0, expr_1.evaluateToObject(ctx))) {
  // ...
} else if (java.util.Objects.equals(switchVal_0, expr_2.evaluateToObject(ctx))) {
  // ...
} else if (java.util.Objects.equals(switchVal_0, expr_3.evaluateToObject(ctx))) {
  // ...
} else {
  // ...
}

Unfortunately, SwitchDirectiveFeature and other TemplateFeature Instances are not currently supported.

Try directive

A real Java try/catch:

try {
    // ...
} catch (TemplateException __ex) {
    // ...
}

Outputfomat and Settings directive

Both fragments push and pop state on the ProcessContext. The compiler currently emits a captured Fragment call for them (frag_N.process(ctx);) so the exact push/pop semantics of the interpreter are preserved.

User directive and macros

<#macro> definitions produce nothing in the generated body; calls (<@user/> and imported macro calls <@lib.x/>) become frag_N.process(ctx);. Dispatch through the macro registry stays dynamic — Stage 1 does not attempt to inline it.

Include directives

Nothing to emit. Parsed includes have already been inlined into the fragment tree at parse time, so the compiler sees the child fragments directly. Unparsed includes (parse=false) show up as a ConstantFragment and therefore become a straight w.write("…​").

A worked example

Given:

Hello ${name}! <#if active>Active</#if>

Stage 1 produces roughly the following class (whitespace tidied):

import org.freshmarker.core.ProcessContext;
import org.freshmarker.core.Environment;
import org.freshmarker.core.compiler.CompiledTemplate;
import org.freshmarker.core.fragment.Fragment;
import org.freshmarker.core.fragment.TemplateReturnException;
import org.freshmarker.core.model.TemplateObject;
import org.freshmarker.core.model.TemplateMarkup;
import org.freshmarker.core.model.TemplateSequence;
import org.freshmarker.core.model.TemplateLooper;
import org.freshmarker.core.model.TemplateSequenceLooper;
import org.freshmarker.core.model.primitive.TemplateBoolean;
import org.freshmarker.core.model.primitive.TemplateString;
import org.freshmarker.core.model.primitive.TemplatePrimitive;
import org.freshmarker.core.environment.ListEnvironment;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.List;
import org.freshmarker.core.ProcessException;

public class CompiledTemplate_0 extends CompiledTemplate {

  private final TemplateMarkup markup_0;
  private final TemplateObject expr_1;

  public CompiledTemplate_0(Object[] captured) {
    super(captured);
    this.markup_0 = (TemplateMarkup) captured[0];
    this.expr_1 = (TemplateObject) captured[1];
  }

  @Override
  @SuppressWarnings("unchecked")
  public void process(ProcessContext ctx) {
    try {
      Writer w = ctx.getWriter();
      w.write("Hello ");
      w.write(markup_0.evaluateToObject(ctx).getValue());
      w.write("! ");
      if (expr_1.evaluate(ctx, TemplateBoolean.class) == TemplateBoolean.TRUE) {
        w.write("Active");
      }
    } catch (IOException e) {
      throw new ProcessException(e.getMessage());
    }
  }
}

Compare that to the interpreter, which would walk a small tree of ConstantFragmentInterpolationFragment and IfFragment on every render. The compiled version replaces the tree walk with a straight-line method call chain, and the megamorphic Fragment#process call site is gone.

Tests cover every case (interpolations, if/elseif/else, list, switch, try/except, includes, imports) and always assert that template.process(data) and the compiled variant produce the exact same output.

The one-step JIT path

CompiledTemplateProcessor.of(Template) is the convenience wrapper for the JIT case:

Configuration config = new Configuration(CompilerFeature.ENABLED);
Template template = config.builder().getTemplate("hello",
        "Hello ${name}! <#if active>Active</#if>");

CompiledFragment compiled = CompiledTemplateProcessor.of(template);

StringWriter out = new StringWriter();
template.processCompiled(compiled, Map.of("name", "World", "active", true), out);

The AOT hook: captureOnly(…​)

At build time you only need the source. At runtime you only need the captured constructor arguments. AbstractJavaSourceCodeGenerator#captureOnly(BlockFragment) does exactly that: it walks the fragment tree, ignores the emitted code, and returns capturedObjects.toArray().

// Build step (runs once during your Maven build):
JavaSourceCodeGenerator generator = new JavaSourceCodeGenerator();
JavaSourceCodeGenerator.GeneratedSource src = generator.generate(
        "com.example.generated", "HelloTemplate", template.getRootFragment());
Files.writeString(Path.of("target/generated-sources/.../HelloTemplate.java"), src.sourceCode());

// Runtime
Object[] captured = new JavaSourceCodeGenerator().captureOnly(template.getRootFragment());
CompiledFragment compiled = new HelloTemplate(captured);

What Stage 1 doesn’t buy you

  • Expression evaluation still boxes. Every expr_N.evaluate(ctx, TemplateString.class) allocates a TemplateString${a + b} still walks a TemplateOperation node before unwrapping.
  • Data model access still goes through the interpreter. ${items[0].price} still resolves through TemplateVariableTemplateDynamicKeyTemplateDotKey.
  • Macros and user directives remain dynamic.

That is why the JIT-compiled numbers in the previous post were basically identical to the interpreter: Stage 1 only removes the outer dispatch, and on a benchmark that spends most of its time in expression evaluation and formatter code, the remaining cost dominates.

Fixing that requires knowing the type of every subexpression at compile time. That is exactly what the next post is about — the Stage 2 typed compiler uses a TemplateTypeSchema to skip TemplateObject entirely and emit real Java expressions that read from your data model with typed casts.

How is all of this created?

At the heart of the Stage 1 implementation is the JavaSourceCodeGenerator class and its generate method.

public GeneratedSource generate(String packageName, String className, BlockFragment rootFragment) {
  imports.addAll(STANDARD_IMPORTS);

  rootFragment.accept(this);

  Map<String, Object> data = Map.of(
    "packageName", packageName,
    "className", className,
    "imports", imports.stream().sorted().toList(),
    "fields", fields.toString(),
    "ctorBody", ctorBody.toString(),
    "body", "      Writer w = ctx.getWriter();\n" + mainEmitter,
    "methods", "",
    "hasTryCatch", hasIO);

  String source = CompilerTemplates.render("compiler/compiled-fragment.fmt", data);
  return new GeneratedSource(source, capturedObjects.toArray());
}

The method takes as parameters the root fragment, the desired class and package name under which the target class is to be created. Since the JavaSourceCodeGenerator class is a TemplateVisitor, it can traverse the entire fragment tree below the root fragment. In the process, it collects all the information necessary for generating the Java source code. This includes all required imports, fields, the source code inside the constructor, and a hasIO flag to insert exception handling if needed.

As the JavaSourceCodeGenerator processes the fragment tree, the individual fragments are handled differently. Some are converted into Java source code, while for others, preparations are made so that they can be called directly within the Java class.

For example, the ConstantFragment, which contains static text, is converted.

@Override
public Void visit(ConstantFragment fragment, String value) {
  emitter().emit("w.write(%s);", escapeJavaString2(value));
  hasIO = true;
  return null;
}

For this fragment, the code line w.write(...); is generated, and the fragment’s content is converted from the value parameter into a Java string. Additionally, the hasIO attribute is set to true here because the write method in the source code requires exception handling.

The SettingFragment is a fragment that is not rendered, but is called directly in the Java source code.

@Override
public Void visit(SettingFragment fragment) {
  emitter().emit(captureFragment(fragment) + ".process(ctx);");
  return null;
}

So, in the Java source code, the fragment’s process method is called. What’s still missing here is the reference to the Fragment in the Java source code. This is handled by the captureFragment method, which “captures” this Fragment.

protected String captureFragment(Fragment obj) {
  String fieldName = "frag_" + capturedObjects.size();
  capturedObjects.add(obj);
  fields.append(FIELD_PREFIX_FRAGMENT).append(fieldName).append(";\n");
  ctorBody.append(CTOR_ASSIGN_PREFIX).append(fieldName).append(CAST_FRAGMENT)
    .append(capturedObjects.size() - 1).append("];\n");
  return fieldName;
}

This method accomplishes three important tasks. First, it generates a unique name for the fragment within the generated class. Next, it adds the fragment to the list of captured fragments, which are later passed into the class via the constructor. Finally, it creates an attribute in the class with that name, and the constructor assigns the correct fragment from the passed-in list to that attribute.

That concludes our discussion of the core concepts of Stage 1 compilation. Next time, we’ll look at how the Stage 2 compiler gets rid of the captured objects and completely does away with fragments of the interpreter.

Leave a Comment