View Source Alembic.AST (alembic v0.2.0)
The abstract syntax tree Alembic.Parser produces and Alembic.Evaluator
walks. Every node is a tagged tuple, chosen over structs so pattern
matching in the evaluator stays exhaustive — adding a node type without
handling it produces a compiler warning, not a silent no-op.
This is the AST, not the CST (concrete syntax tree): it discards
punctuation ({{, }}, %}, endif, dash whitespace markers) and keeps
only semantically meaningful structure. See docs/grammar.md for a
worked example showing both representations of the same template.
Node catalog
| Node | Shape | Example source |
|---|---|---|
text_node() | {:text, content} | Hello, |
output_node() | {:output, expr} | {{ user.name | upcase }} |
if_node() | {:if, cond, then, elsifs, else} | {% if x %}...{% endif %} |
for_node() | {:for, var, iterable, body, else} | {% for i in xs %}...{% endfor %} |
assign_node() | {:assign, var, expr} | {% assign x = 1 %} |
extends_node() | {:extends, name} | {% extends "base.html" %} |
block_node() | {:block, name, body} | {% block title %}...{% endblock %} |
include_node() | {:include, name, vars} | {% include "header.html" %} |
render_node() | {:render, name, vars} | {% render "card.html", title: post.title %} |
break_node() | {:break} | {% break %} |
continue_node() | {:continue} | {% continue %} |
cycle_node() | {:cycle, group, values} | {% cycle "a", "b" %} |
capture_node() | {:capture, var, body} | {% capture x %}...{% endcapture %} |
case_node() | {:case, subject, whens, else} | {% case x %}...{% endcase %} |
Worked example
{{ user.name | upcase }} maps to:
{:output, {:filter_chain, {:variable, ["user", "name"]}, [{:filter, "upcase", []}]}}A non-trivial template:
{% extends "base.html" %}
{% block content %}
Hello, {{ user.name | upcase }}!
{% if user.admin %}(admin){% endif %}
{% endblock %}parses to:
[
{:extends, "base.html"},
{:block, "content", [
{:text, "\n Hello, "},
{:output, {:filter_chain, {:variable, ["user", "name"]}, [{:filter, "upcase", []}]}},
{:text, "!\n "},
{:if, {:variable, ["user", "admin"]}, [{:text, "(admin)"}], [], nil},
{:text, "\n"}
]}
]
Expression nodes (expr())
| Node | Shape | Example |
|---|---|---|
{:variable, path} | variable path | user.name → {:variable, ["user", "name"]} |
{:dynamic, expr} (path segment) | render-time lookup key | items[i] → {:variable, ["items", {:dynamic, {:variable, ["i"]}}]} |
{:literal, value} | literal | 42 → {:literal, 42} |
{:keyword, keyword} | empty/blank operand | x == empty |
{:filter_chain, base, filters} | filtered expression | x | upcase |
{:compare, op, left, right} | comparison | x > 0 |
{:logical, op, left, right} | and/or | a and b |
{:not, expr} | negation | not x |
{:range, from, to} | range literal | (1..5) |
Output tags (output_node()) accept any expression whose base is a
variable path or a literal, optionally wrapped in a filter chain — so
{{ name }}, {{ "hi" | upcase }}, and {{ 42 }} all parse. Bases that
are comparisons, logical operators, or ranges ({{ x > 1 }}) are rejected
by Alembic.Parser with {:unsupported_output_expression, _}.
Summary
Types
@type ast_node() :: text_node() | output_node() | if_node() | for_node() | assign_node() | extends_node() | block_node() | include_node() | render_node() | break_node() | continue_node() | cycle_node() | capture_node() | case_node()
@type break_node() :: {:break}
@type compare_op() :: :eq | :neq | :gt | :lt | :gte | :lte | :contains
@type continue_node() :: {:continue}
@type expr() :: {:variable, path()} | {:literal, literal()} | {:keyword, keyword_literal()} | {:filter_chain, expr(), [filter()]} | {:compare, compare_op(), expr(), expr()} | {:logical, logical_op(), expr(), expr()} | {:not, expr()} | {:range, expr(), expr()}
@type extends_node() :: {:extends, String.t()}
@type keyword_literal() :: :empty | :blank
@type logical_op() :: :and | :or
@type output_node() :: {:output, expr()}
@type path() :: [path_segment()]
@type t() :: [ast_node()]
@type text_node() :: {:text, String.t()}