Introducing hare-template
In my previous post I explained why we needed a much better templating solution for Hare. Let me introduce hare-template, a third party library, and a codegen Hare tool.
The Hare templating syntax is inspired by go-template, adapted to the specific features of Hare.
Once installed, it can be used as:
$ hare tool gentmpl some/module # read all files for annotation
$ hare tool gentmpl some/module/file.ha # read this file for annotation
The templates are annotated string declarations:
// foo/bar.ha
#[template::gen(name: str = "World")]
export def hello_world = "Hello {{ name }}";
The codegen tool will produce this file:
// foo/bar_templategen.ha
// Generated by hare-gentmpl, do not modify by hand
use fmt;
use io;
use strings;
use memio;
// Renders template for printing and writes it into a heap-allocated string.
// The caller must free the return value.
export fn asprint_hello_world(
name: str = "World",
) (str | nomem) = {
let buf = memio::dynamic();
match (fprint_hello_world(&buf, name)) {
case void => void;
case let e: io::error =>
return e as nomem;
};
return strings::fromutf8_unsafe(memio::buffer(&buf));
};
// Renders template for printing and writes it to an [[io::handle]].
export fn fprint_hello_world(
_out: io::handle,
name: str = "World",
) (void | io::error) = {
const in = strings::toutf8(hello_world);
io::writeall(_out, in[0..6])?;
fmt::fprint(_out, name)?;
};
The asprint_hello_world method is an easy way to produce a statically allocated string from a template, but the main focus is on fprint_hello_world. As you can see, the templating relies on slicing expression of the template part to write "Hello ". This to avoid adding any additional stack allocated strings to the built binaries.
Let’s make a quick tour of the features:
All text outside the delimiters are writen without any parsing. But the text inside of them is formated using [[fmt::print]]:
The given {{ src.count }} inventory item is made of {{ src.material }}.
But the givens {{{ brackets are interpretated as {{.
Triming delimiter can be used to trim white spaces before, or after the delimiters:
"{{23 -}} < {{- 45}}" would produce "23<45"
The definition of white space characters is the same as in [[strings::trim]]: space, horizontal tab, carriage return, and newline.
The text after double-slash is considered as comments, and ignored:
{{- // This is some comment }}
Variables assignments follow the Hare syntax:
{{- let foo = "foo"; const bar = foo; }}
Heap allocated variables can be freed using defer:
{{- const pagetitle = fmt::asprint("{} - {}", w.title, p.title)!; }}
{{- defer free(pagetitle); }}
The template can be scoped behind conditions:
{{- if (src.material == "gold") }}
This is made gold!
{{- else if (src.material == "iron") }}
Just some iron...
{{- else }}
Eh, what is that...
{{- end -}}
It is possible to loop over template parts:
{{- for (let i = 0z; i < src.count; i += 1) }}
And for {{ i }}: hip hip hip!
{{- end }}
Match and Switch case are also supported:
{{- match (src.color) }}
{{- case void }}
No color
{{- case let color: str }}
{{ color }}
{{- end -}}
{{- switch (src.material) }}
{{- case "gold" }}
GOLD
{{- case "iron" }}
Some iron...
{{- case end -}}
Note that the last case is immediately followed by a end. This produces an empty block, and is resolved as a simple void;.
Use Break and Continue to stop or skip loops:
{{- for (let i = 0z; i < src.count; i += 1) }}
{{- if (i == 2) continue end }}
{{- if (i == 3) }}
A lot of hips!
{{- continue end }}
And for {{ i }}: hip hip hip!
{{- if (i == 5) break end }}
{{- end -}}
There is no way to define sub-templates yet, but a well-known variable "_out" is available, and is the [[io::handle]] out. If you want to avoid static allocations by using the asprint_ methods, you can use variable assignment to call a method without printing the returned as a [[fmt::formattable]]:
{{ const _ = fprint_another_template(_out); }}
As you noted, in-bracket commands can be chained together, so this template is valid:
#[template::gen(bar: []str)]
export def example = `{{ for (let foo .. bar) if (foo != "") foo else "empty string" end end }}`;
The tool fails when detecting most of the syntax errors, and will point on nesting scope issues, empty blocks, or invalid combinations. But there remains some few exceptions, where it does not check the syntax at all (yet)_. For example, in condition parentheses, variable assignations, defer expressions. The produced code can be invalid, and would be reported by the Hare compiler itself. I’m planning to improve the lexer soon to fix that.
If this post inspired you, feels free to leave a comment!
Reach me