Skip to content.

Acutis language manual

An Acutis template is made of three basic building blocks:

Most of the language features occur within {% expressions %}.

Props and bindings

Every template accepts a map of properties (props) that binds values to names.

In this document, we will use JSON to represent our input data, although Acutis is flexible enough to allow other types of input too.

As an example, the JSON object {"color": "blue"} binds the string "blue" to the name color.

Bindings in Acutis are what some languages call “variables.” You may see the two terms used interchangeably.

Echoing values

If you apply the data from the last section to this template:

My favorite color is {{ color }}.

It renders:

My favorite color is blue.

Nullish coalescing

The ? (question mark) echoes the value on its right-hand side if the value on its left-hand side is null.

My favorite color is {{ color ? fallbackColor }}.

You can chain ?s: {{ a ? b ? "If this prints, a and b are both null." }}

Escaping

Acutis escapes echoes by default. It transforms the following characters into HTML entities:

& " ' > < / ` =

If you use triple-mustaches, {{{ and }}}, then Acutis will not escape the value.

My favorite color is {{{ color }}}.

Format specifiers

To echo numbers or boolean values, then you must prefix them with a format specification. If you’ve ever used “printf” style tools, then this will look similar.

A format specifier begins with a % (percent) followed a character code. These are the specifiers and their types:

Example data:

{ "num": 123456, "frac": 1234.56789, "binaryf": false, "binaryt": true }

Example template usage:

Format specification  Output
-------------------- ------------
{{ %i num }} 123456
{{ %f frac }} 1234.56789
{{ %b binaryf }} false
{{ %b binaryt }} true

These format specifications are designed to cover only basic use cases. If you require more advanced format rules, then you will need to preprocess your data or use component functions.

Comments

{* TODO: add more colors. *}

Anything wrapped {* and *} is always ignored in the output. It is possible to nest comments, similar to ML-style languages.

Whitespace control

The ~ (tilde) symbol trims whitespace before or after an echo or expression.

<p>
{{~ color ~}}
</p>

Renders:

<p>Blue</p>

Trimming whitespace only applies to the template text, not echoed values. If color in the previous example contained whitespace, e.g. " blue ", then those spaces would still render.

Types and interfaces

Acutis is statically typed, so types are checked at compile time and erased at runtime. The type scheme is mostly based on JSON with some additional features.

Acutis does not need annotations. The compiler can infer the types 100% correctly based on how you use the data in your templates. This is a double-edged sword, though. It means your code is more concise, but, if you write a template incorrectly, the compiler will infer the types in ways you may not expect. You may optionally add an interface to each template which explicitly declares its types.

Interfaces

An interface contains a sequence of prop names and their respective types, following the format prop = type. Each interface begins with {% interface and ends with %}.

Examples:

{% interface
page = {title: string, url: string}
visible = false | true
%}

Interfaces are optional, and they exist mainly for the benefit of us humans who may have difficulty inferring types as well as our computers do. They can act as documentation to make large templates easier to understand.

Once you add an interface to a template, then that interface must include all of the template’s props and their types. If the compiler finds a prop in your template that is not listed in the interface, then it will raise an error.

Interfaces may exist anywhere inside a template. Their location does not affect how the compiler parses them. You may even divide an interface across multiple {% interface %} expressions.

Primitives: int, float, and string

Examples:

{% interface
a = int
b = float
c = string
%}
1
1.5
"abc"

The three “primitive” types are int, float, and string, which work as you’d expect from other languages.

Nullable

Examples:

{% interface
a = ?string
%}
null
!"This isn't null."

Any type may be “wrapped” in a nullable. Nullable types are indicated by a ? preceding the wrapped type, e.g. ?int. They can either be null or not-null. The latter is written with a ! (exclamation point).

Because of the way Acutis “wraps” nullable values, they’re closer to what other languages call “option” types.

List

Examples:

{% interface
a = [string]
%}
[]
["a", "b"]
[1, 2, 3, ...rest]

A list is an ordered sequence of values. Lists are homogeneous, so each item must be of the same type. They are indicated with brackets, e.g. [int].

You can append or destructure items from the beginning of a list with the ... syntax: [head, ...tail].

Tuple

Examples:

{% interface
a = (int, string, ?float)
%}
(12, "abc", null)

A tuple is an ordered sequence of items. They’re heterogeneous, so each item may be a different type than its neighbor. They are indicated with parentheses, e.g. (int, string, ?float).

Unlike lists, which are dynamically sized, each tuple has a fixed size defined by its type.

Record

Examples:

{% interface
a = {a: int, b: string}
%}
{a: 12, b: "xyz"}

A record is a series of key-value pairs. Records are indicated by braces, e.g. {a: int, b: string}.

A record field may be quoted, and quotes are always necessary if the field name would not be a valid binding name. These are equivalent: {a: 12} and {"a": 12}.

Records are extensible. The compiler assumes that every record may contain additional fields which have not been specified yet.

When echoing or constructing a value, you may access a record’s field with . (dot) syntax.

{{ book.title }}

Dictionary

Examples:

{% interface
a = <int>
%}
<a: 12, b: 101>

Dictionaries, or “dicts,” are to records what lists are to tuples. They represent key-value pairs like records, but they are dynamically sized and homogeneously typed. They are specified with angled brackets, e.g. <int>.

Like record fields, key names may be quoted.

The order of keys in a dict is not specified. If you require a specific order, then you should use a list.

Enumeration

Examples:

{% interface
a = @"abc" | @"def"
b = @12 | @34 | ...
%}
@"abc"
@12

An enumeration, or an “enum,” is a set of integers or strings. Each enum value is prefixed with an @, e.g. @"a" | @"b" | @"c".

Enums can be “open” or “closed.” A closed enum only allows its specified values. An open enum allows the possibility of adding new values. “Openness” is indicated with ellipses, e.g. @"a" | @"b" | ....

Boolean

Examples:

{% interface
a = false | true
%}
false
true

A boolean is really just a special kind of enum. The false | true type works exactly like any closed binary enum would, such as @0 | @1.

It is possible to declare a type which is only false or only true, but this is not usually useful.

Tagged union

Examples:

{% interface
a =
{@shape: "circle", radius: int} |
{@shape: "rectagle", height: int, width: int}
%}
{@shape: "circle", radius: 12}
{@shape: "rectangle", height: 11, width: 7}

A record may have a “tag” field, indicated by an @, which allows it to unify with other records. This is like a combination of a record and enum type, since the tag field works like an enum.

Unions may be “open” or “closed,” similar to enums.

Tag fields may only contain literal integer, string, or boolean values.

Unknown

{% interface
a = _
%}

Finally, if Acutis can’t determine anything at all about a value’s type, then it uses “unknown,” which is indicated by an underscore: _.

Type design and philosophy

Acutis is strongly and statically typed. The compiler guarantees that values are coherent. For example, a string will never appear where an int is expected.

Acutis is also structurally typed. This allows the compiler to do more than just check type equality. It can check for subtypes, which sees if a particular structure is compatible as a subset of another structure.

The goal is to help real-world data easily fit into a system that’s 100% type-safe. This means you must sometimes make decisions about which types best fit your data. For example, records and tuples are heterogeneous and fixed-sized, while dicts and lists are homogeneous and dynamically-sized. This trade-off is necessary because a structure that is heterogeneous and dynamic would be unsafe.

Types and JSON

The Acutis rendering engine is generic enough to allow different input types, but we’ll use JSON as our primary example. Here are examples of how Acutis types correspond to JSON types and values:

Acutis type JSON type or value
int number
float number
string string
false | true boolean
?string null or string
[string] array with string values
(int, string, float) array with shape [number, string, number]
{a: int, b: string} object with shape {"a": number, "b": string}
<string> object with string values
@"a" | @"b" Values "a" or "b"
@1 | @2 Values 1 or 2
{@tag: "a", b: int} object with shape {"tag": "a", "b": number}
_ (unknown) Any

You may need to preprocess your input data to fit Acutis’ type constraints. For example, if you have a value that can be one of several incompatible types, then you will have to transform it into a legal type such as a tagged union.

Pattern matching

The match and map expressions are the core of Acutis’ powers. They use pattern matching to conditionally output sections of a template.

Pattern matching in Acutis combines object destructuring with equality checking. If you’re used to destructuring objects and using the switch expression in JavaScript, then this may seem like a natural progression from that.

Consider this pattern:

{published: true, title, dates: {posted, updated}}

This matches a record where published equals true, which contains title and dates fields, and where the dates field contains a record with posted and updated fields. Additionally, it binds the values of title, posted, and updated to those names. (This is called “punning,” where the field name is automatically used for a binding with the same name.)

We can use this with the match expression and the with clause:

{% match article
with {published: true, title, dates: {posted, updated}} %}

{{ title }} was posted on {{ posted }} and updated on {{ updated }}.
{% with {published: false} %}
{* Don't render unpublished articles. *}
{% /match %}

The matchwith block is analogous to a switchcase block in some languages. Here, the structure and values of the article binding is checked against the patterns after each with clause. If one of the patterns matches the contents of article, then the following template section is rendered.

Multiple patterns for a block

Acutis allows multiple with patterns to render single block expression.

{% match greeting
with "Hello"
with "Hola"
with "Konnichiwa" %}

I can speak this language.
{% with unknown %}
I don't know what "{{ unknown }}" means.
{% /match %}

Shadowing bindings

Bindings are immutable. Binding a value to an existing name does not override the original, but shadows it. Bindings are scoped to their blocks.

Consider this JSON object:

{ "color": "blue", "other": { "color": "green" } }

And this template:

My favorite is {{ color }}.

{% match other with {color} ~%}
Another is {{ color }}.
{%~ /match %}

But my favorite is still {{ color }}.

Which renders:

My favorite is blue.

Another is green.

But my favorite is still blue.

The top-level color is not affected by the nested color binding.

Exhaustive and partial patterns

The Acutis compiler analyzes patterns to determine whether they are exhaustive or partial. An exhaustive set of patterns covers every possible shape of input data.

It’s easy for a programmer to accidentally write a partial set of patterns, especially when matching a complex data structure. Fortunately, the compiler can handle nested patterns easily. Here’s an example:

{% match author with {name, books: [{title}]} %}
{{ name }}'s latest books is {{ title }}.
{% with {name, books: []} %}
{{ name }} hasn't published any books yet.
{% /match %}

This fails with the error message:

Matching error.
This pattern-matching is not exhaustive.
Here's an example of a pattern which is not matched:
{books: [{title: _}, ..._], name: _}

The example pattern it generated should tell you what you missed. In this case, the original set of patterns only matched a books list with one or zero items, but not more than one (thus the ..._] in the error’s example).

Constructing patterns

Acutis is mainly designed for destructuring data that you already have from an external source. But sometimes it’s useful to construct new data during rendering, especially once you start using template components and need to pass data between different templates. Patterns in Acutis are mostly symmetrical in the sense that any pattern you would use to destructure data can be also used to construct data.

{% match {name: "John", symbol: !"Eagle"}
with {name, symbol: !symbol} ~%}
{{ name }}'s symbol is the {{ symbol }}.
{% with {name, symbol: null} ~%} {{ name }} has no symbol.
{% /match %}

The above example isn’t very practical, but it illustrates how the syntax works.

Template blocks

You can construct template “block” sections. These are denoted with hashes, beginning with #%} and ending with {%#.

Here’s an example using a template component:

{% Layout
sections={
header: #%}
<h1> {{ title }} </h1> {%#,
sidebar: #%}

<h2> Menu </h2>
{% map menu with {title, slug} %}
<a href="{{ slug }}">{{ title }}</a>
{% /map %}
{%#
}
/ %}

These blocks evaluate to regular strings. You can combine them with other patterns too. For example, prefixing a block with a ! will wrap it as a nullable, e.g. {optional: !#%}...{%#}.

Since blocks are rendered as regular strings, echoing will still escape them unless you explicitly don’t, e.g. {{{ header }}}.

Mapping

The map expression is similar to match except that it renders each value of a list.

This data:

{
"articles": [
{ "title": "Acutis templates for beginners", "author": "John" },
{ "title": "Level up your Acutis skills", "author": "Carlo" }
]
}

And this template:

{% map articles with {title, author} ~%}
The article "{{ title }}" was written by {{ author }}.
{% /map %}

Will render:

The article "Acutis templates for beginners" was written by John.
The article "Level up your Acutis skills" was written by Carlo.

Just like match, map can take multiple patterns to conditionally render different template sections based on an value’s content.

{% map articles
with {title, author: null} %}

The article "{{ title }}" was written anonymously.
{% with {title, author: !author} %}
The article "{{ title }}" was written by {{ author }}.
{% /map %}

Matching the item index

You can optionally include an item’s index in the pattern. Indices always begin at zero.

{% map articles with {title, author}, index %}
{{ %i index }}. {{ title }} was written by {{ author }}.
{% /map %}

Because the index binding is just another pattern, you can use it to conditionally render sections by matching the index with specific numbers.

{% map articles
with {title}, 0 %}

Our first article is {{ title }}.
{% with {title} %}
{{ title }}
{% /map %}

Mapping dictionaries

You can map dictionaries by using map_dict. The index will be the key that’s associated with each value.

{% map_dict
<author: {name: "John"},
editor: {name: "Carlo"}>
with {name}, role ~%}

{{ name }} is the {{ role }}.
{% /map_dict %}

Keep in mind that the order of keys in a dictionary is not specified, and may not render in the same order that you write it. If you need a specific order, then use a list.

More about pattern matching

Pattern matching combines many concepts into one terse syntax. In other languages, you may use variable assignment, ifelse expressions, and imperative loops to control how your content is rendered. To help you maximize your use of patterns, this section explains the system’s nuances.

Patterns work by comparing literal values. The following code may not work as you might expect: {% match a with b %}. It does not compare the value of a with value of b, but rather binds the value of a to a new name b.

Reference table

Pattern Matches
_ Any value.
x Any value, and the value is bound to x.
true Exactly true.
false Exactly false.
"abc" The exact string "abc".
1.5 The exact float 1.5.
7 The exact integer 7.
null A nullable value which is null.
!x, !1.5, !"abc", etc. A nullable value which is not null.
@7 The exact integer 7, when 7 is part of an enum.
@"a" The exact string "a", when "a" is part of an enum.
[] A list with exactly zero items.
["a", "b"] A list with exactly two items, "a" and "b" receptively.
["a", "b", ...rest] A list with at least two items, "a" and "b". The remainder of the list is bound to rest.
{a: 1} A record with field a containing integer 1.
{a} A record with field a whose value is then bound to name a.
{a: x} A record with field a whose value is then bound to name x.
{"null": a} A record with field "null" whose value is then bound to name a. (null without quotes is a reserved keyword.)
(7, "a") A 2-tuple whose first value is 7 and whose second value is "a"
{@tag: "a", b: 7} A tagged record with a tag field tag whose value is "a" and with field "b" whose value is 7.
<a: 2> A dictionary with key a containing integer 2.

Ignoring values with _ (underscore)

Bindings will match any value, so you can use them as “catch-all” patterns. However, Acutis will not let you reuse a binding in a pattern, so a pattern like {a: x, b: x} is illegal.

Acutis treats the _ (underscore) name specially. Values bound to it are immediately discarded. Therefore {a: _, b: _} will always match any record with fields a and b, but it will ignore their contents.

Because bindings match anything, you can use _ as a catch-all “default” case:

{% match greeting
with "Hello" %}

This is an English greeting.
{% with "Hola" %}
This is a Spanish greeting.
{% with _ %}
This is some other kind of greeting.
{% /match %}

Ignoring named bindings

The compiler will raise an error if you declare a binding but never use it. You can suppress this warning by prefixing the name with an _ (underscore).

{% match list with [head, ...tail] %}
The list's head is {{ head }}.
This will fail to compile. We forgot to use 'tail'.
{% with [] %} The list is empty.
{% /match %}
{% match list with [head, ..._tail] %}
The list's head is {{ head }}.
This will compile without an error.
{% with [] %} The list is empty.
{% /match %}

Matching multiple values at once

You can match many values at once by separating them with a comma (,). This can be useful for reasoning about two-dimensional matrices of data.

{% match object, color
with "sky", "blue"
with "grass", "green" %}

That's a common color.
{% with _, _ %}
That's an unusual color.
{% /match %}

Matching dictionaries

Matching dictionaries works a little differently than the other types. Because a dictionary is never guaranteed to contain any specific key, dictionary patterns always need a catch-all case.

Additionally, dictionary pattern-matching essentially works by testing for a subset of its input dictionary. This is different than lists, which test for an exact structure. For example, a pattern like <john: "eagle"> matches any dictionary that contains that key and value, like <john: "eagle", luke: "ox">. As a consequence, an empty dictionary, <>, will match any input.

Template components

Template components in Acutis are analogous to “partials” or “includes” in other template languages. They are how we reuse common pieces of templates.

We can also call external functions as templates, which makes them analogous to “filters” or “shortcodes” in other languages. Functional template components can be written the language you use to run Acutis (i.e. OCaml or JavaScript) using the Acutis API.

Components always begin with a capital letter. They accept XML-style props which are turned into bindings within the component. They end with an XML-style / (backslash).

A couple of basic components:

File: Byline.acutis

Written by {{ name }}.

File: Articles.acutis

{% map articles with {title, author} %}
{{ title }} {% Byline name=author / %}
{% /map %}

Prop punning

Like record fields, props can be “punned.” You can take code such as {% DateTime date=date / %} and abbreviate it to {% DateTime date / %}.

Optional props

If a prop is nullable, then you may exclude it when you call its component. The examples below are all valid.

An implicit null prop:

{% Layout %} content {% /Layout %}

An explicit null prop:

{% Layout optional=null %} content {% /Layout %}

An explicit not-null prop:

{% Layout optional=!"this isn't null" %} content {% /Layout %}

Notice how an explicitly not-null prop still requires the ! before its value.

Default children prop

A template section inside a component is automatically bound to a prop named children.

An implicit children prop:

{% Layout %} content {% /Layout %}

An explicit children prop:

{% Layout children=#%} content {%# / %}

Both of those examples will render identically.