Template AST Specification
View SourceThis document defines the Abstract Syntax Tree (AST) structure for Prana's template engine V3.
Overview
The Prana template AST uses a unified tuple format for all nodes:
{type :: atom(), parts :: list(), opts :: keyword()}Where:
type- The node type (:text,:literal,:expression,:tag)parts- Type-specific data as a listopts- Optional metadata as a keyword list
Core Node Types
1. Text Nodes
Represent raw, static text content from the template source.
Format:
{:text, [string_content], opts}Parameters:
string_content- Raw text content as it appears in the template
Examples:
# Template text: "Hello World"
{:text, ["Hello World"], []}
# Whitespace and newlines
{:text, ["\n "], []}
# With source location
{:text, ["Welcome!"], [line: 5, column: 10]}2. Literal Nodes
Represent constant values within expressions (numbers, strings, booleans, nil).
Format:
{:literal, [value], opts}Parameters:
value- Any constant value (string, number, boolean, nil)
Examples:
# Number in expression: 42
{:literal, [42], []}
# String in expression: "admin"
{:literal, ["admin"], []}
# Boolean: true
{:literal, [true], []}
# Null value
{:literal, [nil], []}3. Expression Nodes
Represent variable interpolation and expressions in {{ }} blocks.
Format:
{:expression, [expression_tuple], opts}Parameters:
expression_tuple- A parsed expression tuple (see Expression Types below)
Examples:
# {{ $input.name }}
{:expression, [{:variable, ["$input", "name"], []}], []}
# {{- user.age >= 18 -}}
{:expression, [{:binary_op, [:>=,
{:variable, ["user", "age"], []},
{:literal, [18], []}
], []}], [trim_left: true, trim_right: true]}4. Tag Nodes
Represent control flow and other template tags in {% %} blocks.
Format:
{:tag, [subtype, condition_or_params, body], opts}Parameters:
subtype- Tag type (:if,:for,:assign, etc.)condition_or_params- Parsed expression or parameters for the tagbody- List of child AST nodes
Examples:
# {% if user.active %}Content{% endif %}
{:tag, [:if, {:variable, ["user", "active"], []}, [
{:text, ["Content"], []}
]], []}
# {%- for item in items -%}
{:tag, [:for, {:for_loop, ["item", {:variable, ["items"], []}, []], []}, [
# body nodes...
]], [trim_left: true, trim_right: true]}Expression Types
All expressions use the unified tuple format and can be nested within each other.
Variable Access
Access variables and object properties.
Format:
{:variable, [path_segments], opts}Examples:
# $input
{:variable, ["$input"], []}
# user.name
{:variable, ["user", "name"], []}
# $input.users[0].email
{:variable, ["$input", "users", 0, "email"], []}
# $nodes.api_call.response.data
{:variable, ["$nodes", "api_call", "response", "data"], []}Binary Operations
Comparison and arithmetic operations between two expressions.
Format:
{:binary_op, [operator, left_expr, right_expr], opts}Supported Operators:
- Comparison:
:==,:!=,:>,:>=,:<,:<= - Arithmetic:
:+,:-,:*,:/,:% - String:
:contains
Examples:
# user.age >= 18
{:binary_op, [:>=,
{:variable, ["user", "age"], []},
{:literal, [18], []}
], []}
# user.role == "admin"
{:binary_op, [:==,
{:variable, ["user", "role"], []},
{:literal, ["admin"], []}
], []}
# name contains "John"
{:binary_op, [:contains,
{:variable, ["name"], []},
{:literal, ["John"], []}
], []}Logical Operations
Boolean operations between expressions.
Format:
{:logical_op, [operator, left_expr, right_expr], opts}Supported Operators:
:and- Logical AND:or- Logical OR
Examples:
# user.active and user.age >= 18
{:logical_op, [:and,
{:variable, ["user", "active"], []},
{:binary_op, [:>=,
{:variable, ["user", "age"], []},
{:literal, [18], []}
], []}
], []}
# role == "admin" or role == "moderator"
{:logical_op, [:or,
{:binary_op, [:==, {:variable, ["role"], []}, {:literal, ["admin"], []}], []},
{:binary_op, [:==, {:variable, ["role"], []}, {:literal, ["moderator"], []}], []}
], []}Call Expressions
Apply filters/functions using either pipe or function call syntax. Both syntaxes are unified into a single :call node where the function name is followed by all arguments in order.
Format:
{:call, [function_name, arguments_list], opts}Examples:
Pipe Syntax Transformations:
# user.name | upper_case
{:call, ["upper_case", [{:variable, ["user", "name"], []}]], []}
# user.bio | truncate(50)
{:call, ["truncate", [
{:variable, ["user", "bio"], []},
{:literal, [50], []}
]], []}
# price | format_currency("USD") | round(2)
# Becomes nested calls:
{:call, ["round", [
{:call, ["format_currency", [
{:variable, ["price"], []},
{:literal, ["USD"], []}
]], []},
{:literal, [2], []}
]], []}Function Call Syntax:
# capitalize(user.name)
{:call, ["capitalize", [{:variable, ["user", "name"], []}]], []}
# truncate(user.bio, 50)
{:call, ["truncate", [
{:variable, ["user", "bio"], []},
{:literal, [50], []}
]], []}
# format_currency(price, "USD")
{:call, ["format_currency", [
{:variable, ["price"], []},
{:literal, ["USD"], []}
]], []}Array Literals
Array literals allow inline array construction within templates.
Format:
{:literal, [array_elements], opts}Where array_elements is a list of AST expression nodes that will be evaluated.
Examples:
# Empty array: []
{:literal, [[]], []}
# Number array: [1, 2, 3]
{:literal, [
[{:literal, [1], []}, {:literal, [2], []}, {:literal, [3], []}]
], []}
# String array: ["a", "b", "c"]
{:literal, [
[{:literal, ["a"], []}, {:literal, ["b"], []}, {:literal, ["c"], []}]
], []}
# Mixed type array: [1, "two", true]
{:literal, [
[{:literal, [1], []}, {:literal, ["two"], []}, {:literal, [true], []}]
], []}
# Array with variables: [user.name, user.email]
{:literal, [
[{:variable, ["user", "name"], []}, {:variable, ["user", "email"], []}]
], []}
# Nested arrays: [[1, 2], [3, 4]]
{:literal, [
[
{:literal, [[{:literal, [1], []}, {:literal, [2], []}]], []},
{:literal, [[{:literal, [3], []}, {:literal, [4], []}]], []}
]
], []}Tag Subtypes
Control Flow Tags
If/Elsif/Else:
# {% if condition %}...{% endif %}
{:tag, [:if, [
{condition_expr, body_nodes}
]], opts}
# {% if a %}b{% elsif c %}d{% else %}e{% endif %}
{:tag, [:if, [
{{:variable, ["a"], []}, [{:text, ["b"], []}]},
{{:variable, ["c"], []}, [{:text, ["d"], []}]},
{:else, [{:text, ["e"], []}]}
]], opts}
# Complex example with multiple elsif clauses
# {% if user.role == "admin" %}Admin{% elsif user.role == "mod" %}Moderator{% elsif user.active %}User{% else %}Guest{% endif %}
{:tag, [:if, [
{{:binary_op, [:==, {:variable, ["user", "role"], []}, {:literal, ["admin"], []}], []}, [{:text, ["Admin"], []}]},
{{:binary_op, [:==, {:variable, ["user", "role"], []}, {:literal, ["mod"], []}], []}, [{:text, ["Moderator"], []}]},
{{:variable, ["user", "active"], []}, [{:text, ["User"], []}]},
{:else, [{:text, ["Guest"], []}]}
]], opts}For Loops:
# {% for item in collection %}...{% endfor %}
{:tag, [:for, "item", {:variable, ["collection"], []}, body_nodes, []], opts}
# {% for user in users limit: 5 offset: 10 %}...{% endfor %}
{:tag, [:for, "user", {:variable, ["users"], []}, body_nodes, [limit: 5, offset: 10]], opts}
# {% for item in items limit: 20 %}...{% endfor %}
{:tag, [:for, "item", {:variable, ["items"], []}, body_nodes, [limit: 20]], opts}Utility Tags
Assignment:
# {% assign name = value %}
{:tag, [:assign, "name", value_expr, []], opts}
# {% assign total = price + tax %}
{:tag, [:assign, "total", {:binary_op, [:+,
{:variable, ["price"], []},
{:variable, ["tax"], []}
], []}, []], opts}Options (Keyword Lists)
All AST nodes support optional metadata via keyword lists.
Whitespace Control Options
trim_left - boolean()
Remove whitespace before this node (from {%- or {{- syntax).
trim_right - boolean()
Remove whitespace after this node (from -%} or -}} syntax).
Examples:
# {%- if condition -%}
{:tag, [:if, condition, body], [trim_left: true, trim_right: true]}
# {{- variable -}}
{:expression, [variable_expr], [trim_left: true, trim_right: true]}Source Location Options
line - integer()
Source line number for error reporting.
column - integer()
Source column number for error reporting.
source_file - String.t()
Source template file path.
Examples:
{:text, ["Hello"], [line: 1, column: 1]}
{:literal, [42], [line: 3, column: 15]}
{:expression, [expr], [line: 5, column: 10, source_file: "user.liquid"]}Performance Options
cache_key - String.t()
Cache key for expression evaluation.
static - boolean()
Mark expression as static (no variable dependencies).
Examples:
{:literal, ["constant"], [static: true]}
{:expression, [complex_expr], [cache_key: "user_permissions_check"]}Error Handling Options
strict_mode - boolean()
Controls error handling behavior for incomplete expressions and undefined variables.
true- Return errors for incomplete expressions and undefined variablesfalse(default) - Graceful degradation (render incomplete expressions as-is, treat undefined variables as nil)
Examples:
# With strict_mode: true, {{ undefined_var }} would error
{:expression, [{:variable, ["undefined_var"], []}], [strict_mode: true]}
# With strict_mode: false, incomplete {{ variable would render as-is
{:expression, [{:variable, ["variable"], []}], [strict_mode: false]}Complete AST Examples
Simple Template
Hello {{ user.name }}!AST:
[
{:text, ["Hello "], []},
{:expression, [{:variable, ["user", "name"], []}], []},
{:text, ["!"], []}
]Conditional with Whitespace Control
Users:
{%- for user in users -%}
{{- user.name -}}
{%- endfor -%}AST:
[
{:text, ["Users:\n"], []},
{:tag, [:for, "user", {:variable, ["users"], []}, [
{:text, ["\n "], []},
{:expression, [{:variable, ["user", "name"], []}], [trim_left: true, trim_right: true]},
{:text, ["\n"], []}
], []], [trim_left: true, trim_right: true]}
]Complex Conditional Logic
{% if user.age >= 18 and user.role == "admin" %}
Welcome {{ user.name | upper_case }}!
{% endif %}AST:
[
{:tag, [:if, [
{{:logical_op, [:and,
{:binary_op, [:>=,
{:variable, ["user", "age"], []},
{:literal, [18], []}
], []},
{:binary_op, [:==,
{:variable, ["user", "role"], []},
{:literal, ["admin"], []}
], []}
], []}, [
{:text, ["\n Welcome "], []},
{:expression, [{:call, ["upper_case", [
{:variable, ["user", "name"], []}
]], []}], []},
{:text, ["!\n"], []}
]}
]], []}
]Implementation Guidelines
AST Node Creation
Use helper functions for creating nodes:
defmodule Prana.Template.V3.AST do
def text_node(content, opts \\ []) do
{:text, [content], opts}
end
def literal_node(value, opts \\ []) do
{:literal, [value], opts}
end
def expression_node(expr, opts \\ []) do
{:expression, [expr], opts}
end
def tag_node(subtype, params, body, opts \\ []) do
{:tag, [subtype, params, body], opts}
end
def variable_expr(path, opts \\ []) do
{:variable, path, opts}
end
endExpression Types Reference
Supported Filter Names
String Filters: upper_case, lower_case, capitalize, truncate, default
Number Filters: round, format_currency
Collection Filters: length, first, last, join, sort, reverse, uniq, slice, contains, compact, flatten, sum, keys, values, group_by, map, filter, reject, dump
Math Filters: abs, ceil, floor, max, min, power, sqrt, mod, clamp
Future Extensions
Planned additions to the AST specification:
- Object literals -
{:object, [key_value_pairs], opts}for inline map construction - Ternary operators -
{:ternary, [condition, if_true, if_false], opts} - Case/when statements -
{:case, [switch_expr, when_clauses], opts} - Custom tag extensions - Plugin system for custom tag types
Version History
- v1.1 - Added array literal support with inline array construction
- v1.0 - Initial AST specification with unified tuple format
- v1.0 - Added whitespace control options
- v1.0 - Defined expression type hierarchy
- v1.0 - Specified tag subtypes and for loop parameters