llama_cpp_gbnf/
validate_gbnf.rs1use std::ffi::CString;
2
3use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_EMPTY_RULE_SET;
4use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_LEFT_RECURSION;
5use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_OK;
6use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_ROOT_SYMBOL_MISSING;
7use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_SYNTAX_ERROR;
8use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_THREW_CXX_EXCEPTION;
9use llama_cpp_bindings_sys::llama_rs_gbnf_validation_status;
10use llama_cpp_bindings_sys::llama_rs_validate_gbnf;
11
12use crate::gbnf_validation_error::GbnfValidationError;
13
14fn validation_status_to_result(
15 status: llama_rs_gbnf_validation_status,
16 root: &str,
17) -> Result<(), GbnfValidationError> {
18 match status {
19 LLAMA_RS_GBNF_VALIDATION_OK => Ok(()),
20 LLAMA_RS_GBNF_VALIDATION_SYNTAX_ERROR => Err(GbnfValidationError::SyntaxError),
21 LLAMA_RS_GBNF_VALIDATION_EMPTY_RULE_SET => Err(GbnfValidationError::EmptyRuleSet),
22 LLAMA_RS_GBNF_VALIDATION_ROOT_SYMBOL_MISSING => {
23 Err(GbnfValidationError::RootSymbolMissing {
24 root: root.to_owned(),
25 })
26 }
27 LLAMA_RS_GBNF_VALIDATION_LEFT_RECURSION => Err(GbnfValidationError::LeftRecursion),
28 LLAMA_RS_GBNF_VALIDATION_THREW_CXX_EXCEPTION => {
29 Err(GbnfValidationError::GrammarEngineThrew)
30 }
31 other => unreachable!("llama_rs_validate_gbnf returned unrecognized status {other}"),
32 }
33}
34
35pub fn validate_gbnf(grammar: &str, root: &str) -> Result<(), GbnfValidationError> {
40 let grammar_cstring = CString::new(grammar).map_err(GbnfValidationError::GrammarContainsNul)?;
41 let root_cstring = CString::new(root).map_err(GbnfValidationError::RootContainsNul)?;
42
43 let status = unsafe { llama_rs_validate_gbnf(grammar_cstring.as_ptr(), root_cstring.as_ptr()) };
44
45 validation_status_to_result(status, root)
46}
47
48#[cfg(test)]
49mod tests {
50 use std::ffi::CString;
51
52 use llama_cpp_bindings_sys::LLAMA_RS_GBNF_VALIDATION_THREW_CXX_EXCEPTION;
53 use llama_cpp_bindings_sys::llama_rs_gbnf_validation_status;
54
55 use super::validate_gbnf;
56 use super::validation_status_to_result;
57 use crate::gbnf_validation_error::GbnfValidationError;
58
59 #[test]
60 fn valid_grammar_is_accepted() {
61 assert_eq!(validate_gbnf(r#"root ::= "yes" | "no""#, "root"), Ok(()));
62 }
63
64 #[test]
65 fn malformed_grammar_is_a_syntax_error() {
66 assert_eq!(
67 validate_gbnf("root ::= (", "root"),
68 Err(GbnfValidationError::SyntaxError)
69 );
70 }
71
72 #[test]
73 fn empty_grammar_has_no_rules() {
74 assert_eq!(
75 validate_gbnf("", "root"),
76 Err(GbnfValidationError::EmptyRuleSet)
77 );
78 }
79
80 #[test]
81 fn grammar_without_root_reports_missing_root() {
82 assert_eq!(
83 validate_gbnf(r#"expr ::= "x""#, "root"),
84 Err(GbnfValidationError::RootSymbolMissing {
85 root: "root".to_owned()
86 })
87 );
88 }
89
90 #[test]
91 fn left_recursive_grammar_is_rejected() {
92 assert_eq!(
93 validate_gbnf(r#"root ::= root "x""#, "root"),
94 Err(GbnfValidationError::LeftRecursion)
95 );
96 }
97
98 #[test]
99 fn grammar_with_interior_nul_is_reported() {
100 let grammar = "root ::= \"a\0b\"";
101
102 assert_eq!(
103 validate_gbnf(grammar, "root").err(),
104 CString::new(grammar)
105 .err()
106 .map(GbnfValidationError::GrammarContainsNul)
107 );
108 }
109
110 #[test]
111 fn root_with_interior_nul_is_reported() {
112 let root = "ro\0ot";
113
114 assert_eq!(
115 validate_gbnf(r#"root ::= "x""#, root).err(),
116 CString::new(root)
117 .err()
118 .map(GbnfValidationError::RootContainsNul)
119 );
120 }
121
122 #[test]
123 fn exception_status_maps_to_grammar_engine_threw() {
124 assert_eq!(
125 validation_status_to_result(LLAMA_RS_GBNF_VALIDATION_THREW_CXX_EXCEPTION, "root"),
126 Err(GbnfValidationError::GrammarEngineThrew)
127 );
128 }
129
130 #[test]
131 #[should_panic(expected = "unrecognized status")]
132 fn unrecognized_status_panics() {
133 let _ = validation_status_to_result(llama_rs_gbnf_validation_status::MAX, "root");
134 }
135}