Skip to main content

docs/reference/ast-specification.md

# Template AST Specification

This 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:

```elixir
{type :: atom(), parts :: list(), opts :: keyword()}
```

Where:
- `type` - The node type (`:text`, `:literal`, `:expression`, `:tag`)
- `parts` - Type-specific data as a list  
- `opts` - Optional metadata as a keyword list

## Core Node Types

### 1. Text Nodes

Represent raw, static text content from the template source.

**Format:**
```elixir
{:text, [string_content], opts}
```

**Parameters:**
- `string_content` - Raw text content as it appears in the template

**Examples:**
```elixir
# 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:**
```elixir
{:literal, [value], opts}
```

**Parameters:**
- `value` - Any constant value (string, number, boolean, nil)

**Examples:**
```elixir
# 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:**
```elixir
{:expression, [expression_tuple], opts}
```

**Parameters:**
- `expression_tuple` - A parsed expression tuple (see Expression Types below)

**Examples:**
```elixir
# {{ $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:**
```elixir
{:tag, [subtype, condition_or_params, body], opts}
```

**Parameters:**
- `subtype` - Tag type (`:if`, `:for`, `:assign`, etc.)
- `condition_or_params` - Parsed expression or parameters for the tag
- `body` - List of child AST nodes

**Examples:**
```elixir
# {% 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:**
```elixir
{:variable, [path_segments], opts}
```

**Examples:**
```elixir
# $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:**
```elixir
{:binary_op, [operator, left_expr, right_expr], opts}
```

**Supported Operators:**
- Comparison: `:==`, `:!=`, `:>`, `:>=`, `:<`, `:<=`
- Arithmetic: `:+`, `:-`, `:*`, `:/`, `:%`
- String: `:contains`

**Examples:**
```elixir
# 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:**
```elixir
{:logical_op, [operator, left_expr, right_expr], opts}
```

**Supported Operators:**
- `:and` - Logical AND
- `:or` - Logical OR

**Examples:**
```elixir
# 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:**
```elixir
{:call, [function_name, arguments_list], opts}
```

**Examples:**

**Pipe Syntax Transformations:**
```elixir
# 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:**
```elixir
# 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:**
```elixir
{:literal, [array_elements], opts}
```

Where `array_elements` is a list of AST expression nodes that will be evaluated.

**Examples:**
```elixir
# 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:**
```elixir
# {% 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:**
```elixir
# {% 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:**
```elixir
# {% 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:**
```elixir
# {%- 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:**
```elixir
{: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:**
```elixir
{: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 variables
- `false` (default) - Graceful degradation (render incomplete expressions as-is, treat undefined variables as nil)

**Examples:**
```elixir
# 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
```liquid
Hello {{ user.name }}!
```

**AST:**
```elixir
[
  {:text, ["Hello "], []},
  {:expression, [{:variable, ["user", "name"], []}], []},
  {:text, ["!"], []}
]
```

### Conditional with Whitespace Control
```liquid
Users:
{%- for user in users -%}
  {{- user.name -}}
{%- endfor -%}
```

**AST:**
```elixir
[
  {: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
```liquid
{% if user.age >= 18 and user.role == "admin" %}
  Welcome {{ user.name | upper_case }}!
{% endif %}
```

**AST:**
```elixir
[
  {: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:**
```elixir
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
end
```

## Expression 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