#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirectiveRole {
Opens {
closer: &'static str,
continuations: &'static [&'static str],
},
Continues { opened_by: &'static [&'static str] },
Closes { opened_by: &'static [&'static str] },
Standalone,
}
#[derive(Debug, Clone, Copy)]
pub struct Directive {
pub keyword: &'static str,
pub role: DirectiveRole,
pub syntax: &'static str,
pub summary: &'static str,
}
impl Directive {
pub fn closer(&self) -> Option<&'static str> {
match self.role {
DirectiveRole::Opens { closer, .. } => Some(closer),
_ => None,
}
}
}
pub static DIRECTIVES: &[Directive] = &[
Directive {
keyword: "if",
role: DirectiveRole::Opens {
closer: "end",
continuations: &["elif", "else"],
},
syntax: "{{ if condition }}",
summary: "Render the body when the condition is truthy.",
},
Directive {
keyword: "elif",
role: DirectiveRole::Continues { opened_by: &["if"] },
syntax: "{{ elif condition }}",
summary: "Alternative branch of the enclosing `{{ if }}`.",
},
Directive {
keyword: "else",
role: DirectiveRole::Continues {
opened_by: &["if", "for"],
},
syntax: "{{ else }}",
summary: "Fallback branch. After `{{ for }}` it renders when the \
iterable is empty.",
},
Directive {
keyword: "end",
role: DirectiveRole::Closes {
opened_by: &["if", "for"],
},
syntax: "{{ end }}",
summary: "Close the enclosing `{{ if }}` or `{{ for }}`.",
},
Directive {
keyword: "for",
role: DirectiveRole::Opens {
closer: "end",
continuations: &["else"],
},
syntax: "{{ for item in items }}",
summary: "Repeat the body for each item. `{{ for key, value in dict }}` \
iterates a dict.",
},
Directive {
keyword: "include",
role: DirectiveRole::Standalone,
syntax: "{{ include \"partial.harn.prompt\" }}",
summary: "Render another template here. `with { name: value }` passes \
bindings to it.",
},
Directive {
keyword: "section",
role: DirectiveRole::Opens {
closer: "endsection",
continuations: &[],
},
syntax: "{{ section \"name\" }}",
summary: "Wrap the body in a capability-adaptive envelope chosen for the \
active model.",
},
Directive {
keyword: "endsection",
role: DirectiveRole::Closes {
opened_by: &["section"],
},
syntax: "{{ endsection }}",
summary: "Close the enclosing `{{ section }}`.",
},
Directive {
keyword: "raw",
role: DirectiveRole::Opens {
closer: "endraw",
continuations: &[],
},
syntax: "{{ raw }}",
summary: "Emit the body verbatim, leaving `{{ }}` untouched.",
},
Directive {
keyword: "endraw",
role: DirectiveRole::Closes {
opened_by: &["raw"],
},
syntax: "{{ endraw }}",
summary: "Close the enclosing `{{ raw }}`.",
},
];
pub fn block_keywords() -> Vec<&'static str> {
DIRECTIVES.iter().map(|d| d.keyword).collect()
}
pub fn directive(keyword: &str) -> Option<&'static Directive> {
DIRECTIVES.iter().find(|d| d.keyword == keyword)
}
pub const CLAUSE_KEYWORDS: &[&str] = &["in", "with"];
pub const OPERATOR_KEYWORDS: &[&str] = &["and", "or", "not"];
pub const LITERAL_KEYWORDS: &[&str] = &["true", "false", "nil"];
pub fn filter_names() -> Vec<&'static str> {
super::filters::FILTERS.iter().map(|f| f.name).collect()
}
pub const SECTIONS: &[&str] = &[
"task",
"examples",
"output_format",
"tools",
"thinking_scaffold",
"chain_of_thought",
"system_framing",
];
#[cfg(test)]
mod tests {
use super::*;
use crate::stdlib::template::validate_template_syntax;
#[test]
fn declared_words_are_unique() {
let mut seen = std::collections::BTreeSet::new();
for word in block_keywords()
.into_iter()
.chain(CLAUSE_KEYWORDS.iter().copied())
.chain(OPERATOR_KEYWORDS.iter().copied())
.chain(LITERAL_KEYWORDS.iter().copied())
{
assert!(seen.insert(word), "keyword `{word}` declared twice");
}
let mut filters = std::collections::BTreeSet::new();
for f in filter_names() {
assert!(filters.insert(f), "filter `{f}` declared twice");
}
let mut sections = std::collections::BTreeSet::new();
for s in SECTIONS {
assert!(sections.insert(*s), "section `{s}` declared twice");
}
}
#[test]
fn parser_recognizes_every_block_keyword() {
let mut proved: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
for (keywords, well_formed) in [
(&["if", "end"][..], "{{ if true }}x{{ end }}"),
(&["elif"][..], "{{ if false }}x{{ elif true }}y{{ end }}"),
(&["else"][..], "{{ if false }}x{{ else }}y{{ end }}"),
(&["for"][..], "{{ for x in items }}{{ x }}{{ end }}"),
(&["include"][..], "{{ include \"other.prompt\" }}"),
(
&["section", "endsection"][..],
"{{ section \"task\" }}body{{ endsection }}",
),
(
&["raw", "endraw"][..],
"{{ raw }}{{ not a directive }}{{ endraw }}",
),
] {
let result = validate_template_syntax(well_formed);
assert!(
result.is_ok(),
"declared keyword(s) {keywords:?} failed to parse: {result:?}"
);
proved.extend(keywords);
}
for (keyword, stray) in [
("end", "{{ end }}"),
("endsection", "{{ endsection }}"),
("else", "{{ else }}"),
("elif", "{{ elif true }}"),
] {
let Err(err) = validate_template_syntax(stray) else {
panic!("a stray `{keyword}` should not parse");
};
assert!(
err.contains(keyword),
"stray `{keyword}` did not raise a keyword-specific error: {err}"
);
proved.insert(keyword);
}
let declared: std::collections::BTreeSet<&str> = block_keywords().into_iter().collect();
assert_eq!(
declared, proved,
"every declared block keyword needs a case proving the engine still \
implements it, and every case needs its keyword declared"
);
}
#[test]
fn every_declared_pairing_is_what_the_parser_accepts() {
for (keyword, body) in [
("if", "{{ if a }}\nx\n{{ end }}"),
("for", "{{ for x in xs }}\nx\n{{ end }}"),
("section", "{{ section \"task\" }}\nx\n{{ endsection }}"),
("raw", "{{ raw }}\nx\n{{ endraw }}"),
] {
let closer = directive(keyword)
.and_then(Directive::closer)
.unwrap_or_else(|| panic!("`{keyword}` should declare a closer"));
assert!(
body.contains(&format!("{{{{ {closer} }}}}")),
"`{keyword}` declares `{closer}`, which the sample does not use"
);
assert!(
validate_template_syntax(body).is_ok(),
"`{keyword}` closed by `{closer}` should parse"
);
}
}
#[test]
fn end_prefixed_spellings_close_nothing() {
for wrong in ["endif", "endfor"] {
assert!(
directive(wrong).is_none(),
"`{wrong}` must not be suggestable"
);
assert!(
validate_template_syntax(&format!("intro\n{{{{ {wrong} }}}}\n")).is_ok(),
"a stray `{wrong}` parses as a variable — that is the trap"
);
}
let err = validate_template_syntax("{{ if a }}\nx\n{{ endif }}")
.expect_err("`{{ endif }}` does not close `{{ if }}`");
assert!(err.contains("missing matching"), "unexpected error: {err}");
}
#[test]
fn parser_recognizes_every_clause_keyword() {
assert!(CLAUSE_KEYWORDS.contains(&"in"));
assert!(validate_template_syntax("{{ for x in items }}{{ x }}{{ end }}").is_ok());
assert!(
validate_template_syntax("{{ for x of items }}{{ x }}{{ end }}").is_err(),
"`in` is no longer the for-loop separator"
);
assert!(CLAUSE_KEYWORDS.contains(&"with"));
assert!(validate_template_syntax("{{ include \"p\" with { item: x } }}").is_ok());
}
#[test]
fn expression_lexer_recognizes_operators_and_literals() {
for word in OPERATOR_KEYWORDS {
let src = match *word {
"not" => "{{ if not true }}x{{ end }}".to_string(),
other => format!("{{{{ if true {other} false }}}}x{{{{ end }}}}"),
};
assert!(
validate_template_syntax(&src).is_ok(),
"declared operator `{word}` no longer parses"
);
}
for word in LITERAL_KEYWORDS {
let src = format!("{{{{ if {word} }}}}x{{{{ end }}}}");
assert!(
validate_template_syntax(&src).is_ok(),
"declared literal `{word}` no longer parses"
);
}
}
#[test]
fn parser_accepts_exactly_the_declared_sections() {
for name in SECTIONS {
let src = format!("{{{{ section \"{name}\" }}}}body{{{{ endsection }}}}");
assert!(
validate_template_syntax(&src).is_ok(),
"declared section `{name}` is not accepted by the parser"
);
}
let err = validate_template_syntax("{{ section \"nope\" }}b{{ endsection }}")
.expect_err("undeclared section should be rejected");
assert!(
err.contains("unknown template section"),
"unexpected error for undeclared section: {err}"
);
}
}