How It Works
The dependency graph, resolver, and modifier pipeline explained.
Sintax is built around three ideas: parse expressions into tokens, build a dependency graph across all variables, and apply modifiers as a pipeline.
Parsing
Every string value is scanned for {{ }} expressions. Each expression is tokenised into one of:
| Token | Example |
|---|---|
| Plain variable | {{ name }} |
| Filtered variable | {{ text | trim | upper }} |
| If block | {{ if condition }}...{{ /if }} |
| Ternary | {{ x | gt:0 ? 'yes' : 'no' }} |
The parser extracts variable references and modifier chains but does not evaluate anything yet. This separation lets the resolver build the dependency graph before any rendering happens.
The problem it solves
Most template engines assume you hand them a fully-resolved context. Sintax assumes your context is the thing that needs resolving.
# Input - variables that reference each other
env: "production"
prefix: "{{ env | upper }}"
db_name: "{{ prefix }}_database"
# Output - resolved in dependency order
env: "production"
prefix: "PRODUCTION"
db_name: "PRODUCTION_database"Dependency resolution
Parse - scan every variable value, collect which other variables it references
Build graph - construct a directed acyclic graph (DAG) where edges point from dependency to dependent
Sort - topologically sort the graph to get a safe resolution order
Resolve - process variables one by one, memoising each result so nothing is evaluated twice
Detect cycles - if the graph contains a cycle, return a typed error before any rendering occurs
Because variables are resolved in dependency order, by the time {{ prefix }}_{{ suffix }} is evaluated, both prefix and suffix are already final values.
Modifier pipeline
Each {{ value | mod1 | mod2:arg }} expression runs the value through a left-to-right pipeline:
initial value
→ mod1(value) → result1
→ mod2(result1, [arg]) → result2
→ outputModifier arguments can be:
| Type | Syntax |
|---|---|
| String literal | 'quoted' or "quoted" |
| Number | 42, 3.14 |
| Boolean | true, false, yes, no |
| Variable reference | unquoted name resolved from context |
The default modifier is special - it catches missing-variable and modifier errors and substitutes its argument instead, letting you build resilient pipelines without extra branching.
Nested data
Sintax resolves interpolations recursively through any nested structure:
| Type | Behaviour |
|---|---|
| Maps | keys and values are both resolved |
| Slices | every element is resolved |
| Structs | exported fields are resolved |
| Pointers | dereferenced, then resolved |
This means a slice of maps each containing template strings resolves correctly with no extra configuration.
Conditional logic
{{ if is_admin }}Admin panel{{ else }}Dashboard{{ /if }}
{{ count | gt:0 ? 'items found' : 'nothing here' }}Conditions use any expression that resolves to a truthy value. The gt, gte, eq, and not modifiers are built for this.
Safe rendering
RenderSafe skips re-interpolating variable values. Use it when rendering a template that was already produced by a previous resolution step - it prevents double-interpolation and keeps output stable.
Error model
All errors are typed sentinels matchable with errors.Is:
| Error | When |
|---|---|
ErrVariableNotFound | A referenced variable is not in the context |
ErrCircularDependency | Variable A → B → A detected in the graph |
ErrFunctionNotFound | An unknown modifier name was used |
ErrFunctionApplyFailed | A modifier returned an error |
ErrParseFailed | The template expression could not be parsed |
ErrRenderFailed | Execution failed during rendering |
Special variables
A handful of names are resolved dynamically before the dependency graph runs:
| Name | Produces |
|---|---|
{{ uuid }} | UUID v4 string |
{{ uuidv1 }} | UUID v1 string |
{{ now }} | current date-time - pipe into format |
{{ [] }} | empty array - useful with default |
{{ {} }} | empty object - useful with default |
Extending the modifier set
Modifiers are plain functions with a single signature. Pass overrides when creating the engine to add or replace any modifier - custom ones participate in the same pipeline and error model as built-ins.
How is this guide?