View Source Alembic.Parser (alembic v0.1.0)

Recursive descent parser: turns the flat token list produced by Alembic.Lexer into a typed Alembic.AST.t().

Each parse_* function is a direct implementation of a production rule in docs/grammar.md. Expression content (inside {{ }} and after tag keywords) is delegated to Alembic.Parser.Expression.

Alembic.Token.t() carries a line/col position (see Alembic.Token.position/1). Two error reasons below surface one: :unexpected_token names a concrete leftover token, so it uses that token's own position; :missing_end_tag uses the position of the opening tag that's missing its close (e.g. the {% if %} itself), not a closing-tag position, since there's often no closing tag at all to point at — running out of input entirely is the common case, not finding the wrong one. The remaining error reasons here (:malformed_for, :invalid_expression, etc.) operate on already-extracted tag/expression content rather than a token, so they don't carry a position.

Whitespace control is resolved here, not in the Evaluator

Alembic.AST node types have no field for strip_left/strip_right — adding one would mean threading strip intent through every node shape and back out again at render time. Instead, parse/1 resolves whitespace control immediately, as a pass over the raw token list before any node is built: an output/tag token's strip_left/strip_right flag trims the adjacent :text token in place. By the time parse_template/1 builds the AST, the whitespace is already gone — the tree the Evaluator walks needs no strip metadata at all.

Summary

Functions

Turns a token list (from Alembic.Lexer.tokenize/1) into an Alembic.AST.t().

Like parse/1, but tries to keep going after an error instead of stopping at the first one, so a single call can report every independent problem in a template rather than making the caller fix-and-reparse one error at a time. {:ok, ast} only when there are zero errors; {:error, [reason(), ...]} otherwise, one entry per error found, in the order encountered.

Types

@type reason() ::
  {:unexpected_token, Alembic.Token.t(), Alembic.Token.position()}
  | {:missing_end_tag, String.t(), Alembic.Token.position()}
  | :extends_not_first
  | {:unsupported_output_expression, String.t()}
  | {:invalid_expression, String.t(), Alembic.Parser.Expression.reason()}
  | {:malformed_for, String.t()}
  | {:malformed_assign, term()}
  | {:malformed_extends, term()}
  | {:malformed_include, term()}
  | {:unexpected_tag, String.t()}

Functions

@spec parse([Alembic.Token.t()]) :: {:ok, Alembic.AST.t()} | {:error, reason()}

Turns a token list (from Alembic.Lexer.tokenize/1) into an Alembic.AST.t().

Examples

iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}hi{% endif %}")
iex> Alembic.Parser.parse(tokens)
{:ok, [{:if, {:variable, ["x"]}, [{:text, "hi"}], [], nil}]}

iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}no close")
iex> Alembic.Parser.parse(tokens)
{:error, {:missing_end_tag, "endif", %{line: 1, col: 1}}}
@spec parse_all([Alembic.Token.t()]) ::
  {:ok, Alembic.AST.t()} | {:error, [reason(), ...]}

Like parse/1, but tries to keep going after an error instead of stopping at the first one, so a single call can report every independent problem in a template rather than making the caller fix-and-reparse one error at a time. {:ok, ast} only when there are zero errors; {:error, [reason(), ...]} otherwise, one entry per error found, in the order encountered.

Recovery is best-effort, not a guarantee of precise error isolation: on an error, it skips forward until the next {% tag %} or {{ output }} token (or end of input) and resumes from there. A single malformed construct — e.g. an {% if %} with a genuinely broken tag nested inside it — can therefore surface as more than one reported error: the real one, plus a spurious one from resuming mid-construct. Treat the result as "at least these problems exist," not as N precisely-located, independent errors. parse/1 remains the right choice whenever only the first error matters (that's still every existing caller of this module).

Examples

iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}hi{% endif %}")
iex> Alembic.Parser.parse_all(tokens)
{:ok, [{:if, {:variable, ["x"]}, [{:text, "hi"}], [], nil}]}

iex> {:ok, tokens} = Alembic.Lexer.tokenize("{% if x %}a{% endfor %}{{ y ** }}")
iex> Alembic.Parser.parse_all(tokens)
{:error,
 [
   {:missing_end_tag, "endif", %{line: 1, col: 1}},
   {:invalid_expression, "y **", {:unknown_operator, "**"}}
 ]}