Skip to main content

llama_cpp_gbnf/
validate_gbnf.rs

1use 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
35/// # Errors
36///
37/// Returns [`GbnfValidationError`] when `grammar` or `root` contains an interior
38/// NUL byte, or when the grammar parser rejects the grammar.
39pub 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}