Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025
Performance

Runtime performance

How fast the programs Rue compiles actually run. Every point is a whole process — spawn to exit — of a release-quality build, measured on the pinned reference regime and judged against a committed correctness oracle before it is allowed to be a measurement at all. The companion series for the compiler itself is on the compiler performance page.

The ports, side by side

The ratio above divides one tool's time by another's; this is what the two sides are made of. All three excerpts render the specification sidebar, which is where this corpus's work is — 71 of the roughly 96 pages the tools build at 1x are specification pages, and both the active class and the recursion are gated on a prefix test against the current page's permalink, so at 1x the sidebar is a different document on every page. That is a claim about the original corpus only: as the caption above records, duplication collapses it, and at 10x most specification pages share one navigation.

The three say it differently, which is the other reason to put them side by side. Zola runs the production template unchanged — that is what the port is for. Gazette needs four substitutions to say the same thing in a smaller dialect: it has no arithmetic, so the depth parameter goes, and the prefix test is spelled as a filter rather than a test. Only Hugo restates the recursion outright — Go templates have no macros, so what Tera calls on itself as self::render_nav becomes a partial invoking itself with a dict.

Each excerpt is cut from the checked-in file when this page is built, never pasted, and the line numbers below are wherever the block was found in that build. Rename it, split it, or precompute the prefix test out of it, and the extraction fails and this page does not build.

gazetteTera subset
{% macro render_nav(section, current_path) %}
<ul class="spec-nav-list">
    {# Render direct pages in this section (already sorted by weight) #}
    {% for pg in section.pages %}
    <li class="spec-nav-item">
        <a href="{{ pg.permalink }}" class="spec-nav-link {% if pg.permalink == current_path %}active{% endif %}">
            {{ pg.title }}
        </a>
    </li>
    {% endfor %}

    {# Subsections paths are already sorted alphabetically by Zola, which works for
       our numbered chapters (02-lexical-structure, 03-types, etc.) #}
    {% for sub_path in section.subsections | sort %}
        {% set sub = get_section(path=sub_path) %}
    <li class="spec-nav-section">
        <a href="{{ sub.permalink }}" class="spec-nav-section-title {% if current_path | starts_with(prefix=sub.permalink) %}active{% endif %}">
            {{ sub.title }}
        </a>
        {% if current_path | starts_with(prefix=sub.permalink) %}
        {{ self::render_nav(section=sub, current_path=current_path) }}
        {% endif %}
    </li>
    {% endfor %}
</ul>
{% endmacro %}
examples/gazette/templates/spec/base.html, lines 82–107.
ZolaTera
{% macro render_nav(section, current_path, depth) %}
<ul class="spec-nav-list">
    {# Render direct pages in this section (already sorted by weight) #}
    {% for pg in section.pages %}
    <li class="spec-nav-item">
        <a href="{{ pg.permalink }}" class="spec-nav-link {% if pg.permalink == current_path %}active{% endif %}">
            {{ pg.title }}
        </a>
    </li>
    {% endfor %}

    {# Subsections paths are already sorted alphabetically by Zola, which works for
       our numbered chapters (02-lexical-structure, 03-types, etc.) #}
    {% for sub_path in section.subsections | sort %}
        {% set sub = get_section(path=sub_path) %}
    <li class="spec-nav-section">
        <a href="{{ sub.permalink }}" class="spec-nav-section-title {% if current_path is starting_with(sub.permalink) %}active{% endif %}">
            {{ sub.title }}
        </a>
        {% if current_path is starting_with(sub.permalink) %}
        {{ self::render_nav(section=sub, current_path=current_path, depth=depth + 1) }}
        {% endif %}
    </li>
    {% endfor %}
</ul>
{% endmacro %}
performance/ports/zola/templates/spec/base.html, lines 76–101.
HugoGo text/template
{{- $current := .current -}}
<ul class="spec-nav-list">
    {{ range .section.RegularPages }}
    <li class="spec-nav-item">
        <a href="{{ .Permalink }}" class="spec-nav-link {{ if eq .Permalink $current }}active{{ end }}">
            {{ .Title }}
        </a>
    </li>
    {{ end }}

    {{/* Sorted by path, as the Tera macro sorts `section.subsections`. The
         corpus's numbered chapter directories make that the reading order, and
         it stays total on the duplicated corpus of the scale variants, where
         every copy ties with its original on weight and on title. */}}
    {{ range sort .section.Sections "Path" }}
    <li class="spec-nav-section">
        <a href="{{ .Permalink }}" class="spec-nav-section-title {{ if strings.HasPrefix $current .Permalink }}active{{ end }}">
            {{ .Title }}
        </a>
        {{ if strings.HasPrefix $current .Permalink }}
        {{ partial "spec-nav.html" (dict "section" . "current" $current) }}
        {{ end }}
    </li>
    {{ end }}
</ul>
performance/ports/hugo/layouts/_partials/spec-nav.html, lines 21–45.

And what reads them

Zola and Hugo each arrive with a general-purpose template engine. Gazette arrives with this. The prefix test the sidebar gates on is spelled as a filter because gazette's grammar has no is tests, and it runs where Tera's and Go's do — inside the program being measured. Computing the open branch outside the template would move measured work out of the benchmark, and no output check would catch it: a flattened sidebar emits the same links and the same visible text on every page.

The last line is what corpus-specialized means, stated in source rather than in a caption. The filter chain ends in a diagnostic: gazette implements the filters this corpus uses and refuses every other one, where Zola and Hugo implement all of theirs. That is the structural asymmetry the comparison table above discloses, and this is the line it refers to.

gazetteRue
// Tera spells this as the test `x is starting_with(y)`. A filter needs no
// new grammar and reads the same way, and it must exist in the ENGINE: the
// production sidebar gates both the `active` class and its own recursion on
// it, and computing it host-side would move measured work out of the
// program the benchmark exists to measure.
if eng.arena.strings.equals_literal(name, "starts_with") {
    let prefix = arg_of(inout eng, args, "prefix", line);
    if prefix == NONE() {
        fail(inout eng, line, "starts_with requires a `prefix` argument", name);
        return eng.arena.boolean(false);
    }
    let subject = eng.arena.scalar(input);
    let wanted = eng.arena.scalar(prefix);
    return eng.arena.boolean(subject.starts_with(borrow wanted));
}
fail(inout eng, line, "unknown filter", name);
examples/gazette/tmpl_eval.rue, lines 420–435.