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

NodeShapeExample 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())

NodeShapeExample
{:variable, path}variable pathuser.name → {:variable, ["user", "name"]}
{:dynamic, expr} (path segment)render-time lookup keyitems[i] → {:variable, ["items", {:dynamic, {:variable, ["i"]}}]}
{:literal, value}literal42 → {:literal, 42}
{:keyword, keyword}empty/blank operandx == empty
{:filter_chain, base, filters}filtered expressionx | upcase
{:compare, op, left, right}comparisonx > 0
{:logical, op, left, right}and/ora and b
{:not, expr}negationnot 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 assign_node() :: {:assign, String.t(), expr()}
@type block_node() :: {:block, String.t(), [ast_node()]}
@type break_node() :: {:break}
@type capture_node() :: {:capture, String.t(), [ast_node()]}
@type case_node() :: {:case, expr(), [{[expr()], [ast_node()]}], [ast_node()] | nil}
@type compare_op() :: :eq | :neq | :gt | :lt | :gte | :lte | :contains
@type continue_node() :: {:continue}
@type cycle_node() :: {:cycle, String.t() | nil, [expr()]}
@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 filter() :: {:filter, String.t(), [expr()]}
@type for_node() :: {:for, String.t(), expr(), [ast_node()], [ast_node()] | nil}
@type if_node() ::
  {:if, expr(), [ast_node()], [{expr(), [ast_node()]}], [ast_node()] | nil}
@type include_node() :: {:include, String.t(), map()}
@type keyword_literal() :: :empty | :blank
@type literal() :: String.t() | number() | boolean() | nil
@type logical_op() :: :and | :or
@type output_node() :: {:output, expr()}
@type path() :: [path_segment()]
@type path_segment() :: String.t() | {:dynamic, expr()}
@type render_node() :: {:render, String.t(), map()}
@type t() :: [ast_node()]
@type text_node() :: {:text, String.t()}