The Weaver Language
A template language for procedural content generation in Rust. Parse text with embedded expressions, control flow, and host-defined callables, then evaluate it into a final string.
Hello, {{global:name}}! You have {{global:hp}} HP.
{# if {{global:hp}} < 20 #}
You're badly wounded.
{# elif {{global:hp}} < 50 #}
You've seen better days.
{# else #}
You're in fighting shape.
{# endif #}
weaver-lang separates the language (parsing and evaluation) from the host (state, triggers, documents) through a trait boundary. The library has no opinion about where your data lives — you provide it through an EvalContext implementation.
Quick start
use ;
let mut ctx = new;
ctx.set;
let registry = new;
let output = render.unwrap;
assert_eq!;
Compiled templates
Parse once, evaluate many times:
use ;
let template = compile.unwrap;
let registry = new;
let mut ctx = new;
ctx.set;
assert_eq!;
ctx.set;
assert_eq!;
Syntax reference
Overview
| type | syntax |
|---|---|
| variables | {{namespace:value}} |
| processors | @[namespace.name(foo: value1, bar: value2)] |
| commands | $[name(foo, bar)] |
| triggers | <trigger id="some_id"> |
| documents | [[some_id]] |
| if/else | {# if foo == bar #} baz {# endif #} |
| foreach | {# foreach foo in bar #} - {{foo}} {# endforeach #} |
Variables
{{scope:name}}
Look up a variable by scope and name. global and local are conventional, but hosts can define any scope.
Processors — @[namespace.name(key: value)]
Pure computations with named properties. No access to evaluation state.
@[math.add(a: 1, b: 2)]
@[core.weaver.rng(min: 1, max: 100)]
Commands — $[name(arg1, arg2)]
Stateful operations with positional arguments. Can read/write variables through the evaluation context.
$[set_var("global:name", "Alice")]
$[greet("world")]
When a command appears alone on a line, the entire line is consumed — no blank line is left in the output:
Line before
$[set_var("global:x", "val")]
Line after
Evaluates to Line before\nLine after.
Triggers and documents
<trigger id="dark_forest"> // Activate another entry, splice its output
[[LORE_INTRO]] // Import a reusable content block
Both are expressions — they can appear in arrays, processor arguments, conditions, etc.
Control flow
{# if {{global:hp}} < 20 #}
Critical condition!
{# elif {{global:hp}} < 50 #}
Wounded.
{# else #}
Healthy.
{# endif #}
{# foreach item in ["sword", "shield", "potion"] #}
- {{item}}
{# endforeach #}
Control flow tags on their own lines don't produce blank lines in the output.
Expressions and operators
Expressions appear in conditions, arguments, and inline. Types are preserved internally (string, number, bool, array, none) and coerced to strings only at the template output level.
Comparison: ==, !=, <, >, <=, >=
Logical: &&, ||, !
Arithmetic: +, -, *, /
+ concatenates when either operand is a string. Division by zero returns an error.
Truthiness: empty string, 0, false, empty array, and none are falsy. Everything else is truthy.
Precedence (highest to lowest): unary (!, -), arithmetic (*, /, +, -), comparison (==, !=, <, >, <=, >=), logical (&&, ||). Parentheses override precedence.
Registering processors and commands
Closures
use ;
let mut registry = new;
registry.register_processor;
registry.register_command;
Proc macros
The weaver-macros crate generates trait implementations from function signatures with automatic type validation:
use ;
use weaver_processor;
// Generates `RepeatTextProcessor` struct implementing `WeaverProcessor`.
// registry.register_processor(RepeatTextProcessor);
Commands can opt into context access by naming a parameter ctx:
use ;
use weaver_command;
// Generates `SetVarCommand` struct implementing `WeaverCommand`.
Supported parameter types: Value (any), String, f64, bool, Vec<Value>.
Trait implementations
For full control, implement WeaverCommand or WeaverProcessor directly:
use ;
use ;
;
Implementing EvalContext
SimpleContext works for testing. For production, implement the EvalContext trait to connect weaver-lang to your application's state:
use ;
The evaluator manages temporary scopes internally (foreach bindings). Only named scope operations like "global" and "local" reach the host.
Evaluation options
Configure resource limits, cancellation, and lenient mode:
use ;
use Arc;
use AtomicBool;
let mut ctx = new;
let registry = new;
let cancel = new;
let opts = new
.max_node_evaluations // cap AST node evaluations
.max_iterations // cap total loop iterations
.cancellation_token // abort from another thread
.lenient; // undefined vars render as raw syntax
let result = render_with_options.unwrap;
assert_eq!;
Error reporting
Parse errors carry source spans. Use format_with_source for diagnostics:
Error: undefined variable: global:player_name
--> Dark Forest:12:6
|
12 | {# if {{global:player_name}} #}
| ^^^^^^^^^^^^^^^^^^^^^^^
= hint: did you mean to define this variable first?
Eval errors support error chaining for host-originated failures:
use EvalError;
let io_err = new;
let err = host_error.with_source;
// The full error chain is preserved via std::error::Error::source()
Known limitations
- All numbers are
f64. Large integers above 2^53 lose precision. - No assignment syntax in the language. Variable mutation goes through commands which hosts need to define.
- Document evaluation depends on the host's
resolve_documentimplementation.
Dependencies
- pest — PEG parser generator
- thiserror — error derive macros
- syn, quote, proc-macro2 — proc macro infrastructure (weaver-macros only)
License
MIT