use super::converter::Converter;
use super::error::SchemaError;
use super::primitives::build_repetition;
use std::collections::BTreeMap;
const DOT_RULE: &str = r##"[^\x0A\x0D]"##;
fn is_non_literal(c: char) -> bool {
matches!(
c,
'|' | '.' | '(' | ')' | '[' | ']' | '{' | '}' | '*' | '+' | '?'
)
}
fn is_bare_in_gbnf(c: char) -> bool {
matches!(
c,
'^' | '$' | '.' | '[' | ']' | '(' | ')' | '|' | '{' | '}' | '*' | '+' | '?'
)
}
fn is_gbnf_escape(c: char) -> bool {
matches!(c, '\\' | '"' | 'n' | 'r' | 't' | 'x' | 'u' | 'U')
}
fn shorthand_class(c: char) -> Option<&'static str> {
match c {
'd' => Some("[0-9]"),
'D' => Some("[^0-9]"),
'w' => Some("[0-9A-Za-z_]"),
'W' => Some("[^0-9A-Za-z_]"),
_ => None,
}
}
fn shorthand_members(c: char) -> Option<&'static str> {
match c {
'd' => Some("0-9"),
'w' => Some("0-9A-Za-z_"),
_ => None,
}
}
#[derive(Clone)]
struct Piece {
text: String,
literal: bool,
}
impl Piece {
fn rule(text: impl Into<String>) -> Self {
Piece {
text: text.into(),
literal: false,
}
}
fn literal(text: impl Into<String>) -> Self {
Piece {
text: text.into(),
literal: true,
}
}
fn to_rule(&self) -> String {
if self.literal {
format!("\"{}\"", self.text)
} else {
self.text.clone()
}
}
}
fn join_seq(seq: &[Piece]) -> Piece {
let mut merged: Vec<Piece> = Vec::with_capacity(seq.len());
let mut literal = String::new();
for item in seq {
if item.literal {
literal.push_str(&item.text);
} else {
if !literal.is_empty() {
merged.push(Piece::literal(std::mem::take(&mut literal)));
}
merged.push(item.clone());
}
}
if !literal.is_empty() {
merged.push(Piece::literal(literal));
}
Piece::rule(
merged
.iter()
.map(Piece::to_rule)
.collect::<Vec<_>>()
.join(" "),
)
}
pub(super) struct PatternCompiler<'a> {
conv: &'a mut Converter,
chars: Vec<char>,
pos: usize,
name: String,
source: String,
sub_rule_ids: BTreeMap<String, String>,
}
impl<'a> PatternCompiler<'a> {
pub(super) fn compile(
conv: &'a mut Converter,
pattern: &str,
name: &str,
) -> Result<String, SchemaError> {
let chars: Vec<char> = pattern.chars().collect();
if chars.first() != Some(&'^') || chars.last() != Some(&'$') || chars.len() < 2 {
return Err(SchemaError::UnsupportedPattern {
pattern: pattern.to_string(),
why: "a pattern must start with '^' and end with '$'; llama.cpp anchors every \
pattern it compiles, and an unanchored one would match a substring"
.to_string(),
});
}
let mut compiler = PatternCompiler {
conv,
chars: chars[1..chars.len() - 1].to_vec(),
pos: 0,
name: name.to_string(),
source: pattern.to_string(),
sub_rule_ids: BTreeMap::new(),
};
let body = compiler.transform(true)?.to_rule();
let rule = format!("\"\\\"\" ({body}) \"\\\"\"");
Ok(compiler.conv.add_rule(name, &rule))
}
fn refuse(&self, why: impl Into<String>) -> SchemaError {
SchemaError::UnsupportedPattern {
pattern: self.source.clone(),
why: why.into(),
}
}
fn at(&self, i: usize) -> Option<char> {
self.chars.get(i).copied()
}
fn transform(&mut self, top_level: bool) -> Result<Piece, SchemaError> {
let start = self.pos;
let mut seq: Vec<Piece> = Vec::new();
while self.pos < self.chars.len() {
let c = self.chars[self.pos];
match c {
'.' => {
let dot = self.conv.add_rule("dot", DOT_RULE);
seq.push(Piece::rule(dot));
self.pos += 1;
}
'(' => {
self.pos += 1;
if self.at(self.pos) == Some('?') {
if self.at(self.pos + 1) == Some(':') {
self.pos += 2;
} else {
return Err(self.refuse(
"lookahead and lookbehind groups ((?=, (?!, (?<=, (?<!) have no \
GBNF form; llama.cpp warns and then drops the group entirely, \
which would let the grammar accept what the pattern rejects",
));
}
}
let inner = self.transform(false)?;
seq.push(Piece::rule(format!("({})", inner.to_rule())));
}
')' => {
self.pos += 1;
let opened_group = !top_level
&& start > 0
&& (self.chars[start - 1] == '('
|| (start >= 2
&& self.chars[start - 2] == '?'
&& self.chars[start - 1] == ':'));
if !opened_group {
return Err(self.refuse("unbalanced parentheses"));
}
return Ok(join_seq(&seq));
}
'[' => {
let class = self.char_class()?;
seq.push(Piece::rule(class));
}
'|' => {
seq.push(Piece::rule("|"));
self.pos += 1;
}
'*' | '+' | '?' => {
let last = seq
.last_mut()
.ok_or_else(|| SchemaError::UnsupportedPattern {
pattern: self.source.clone(),
why: format!("'{c}' has nothing before it to repeat"),
})?;
*last = Piece::rule(format!("{}{c}", last.to_rule()));
self.pos += 1;
}
'{' => self.repetition(&mut seq)?,
'\\' if self.at(self.pos + 1).and_then(shorthand_class).is_some() => {
let next = self.chars[self.pos + 1];
let class = shorthand_class(next).unwrap_or(DOT_RULE);
seq.push(Piece::rule(class));
self.pos += 2;
}
_ => {
let before = self.pos;
if let Some(literal) = self.literal_run()? {
seq.push(literal);
}
if self.pos == before {
return Err(self.refuse(format!(
"'{c}' has no opening '{}'",
match c {
']' => '[',
_ => '{',
}
)));
}
}
}
}
if !top_level {
return Err(self.refuse("unbalanced parentheses"));
}
Ok(join_seq(&seq))
}
fn literal_run(&mut self) -> Result<Option<Piece>, SchemaError> {
let mut literal = String::new();
let len = self.chars.len();
while self.pos < len {
let c = self.chars[self.pos];
if c == '\\' && self.pos + 1 < len {
let next = self.chars[self.pos + 1];
if is_bare_in_gbnf(next) {
literal.push(next);
self.pos += 2;
} else if shorthand_class(next).is_some() {
break;
} else if is_gbnf_escape(next) {
literal.push('\\');
literal.push(next);
self.pos += 2;
} else {
return Err(self.refuse(format!(
"the escape \"\\{next}\" has no GBNF spelling{}",
match next {
's' | 'S' =>
"; ECMA-262 defines it over Unicode whitespace, and the ASCII \
subset a grammar could write would reject strings the pattern \
accepts",
'b' | 'B' =>
"; word boundaries are zero-width and a grammar has no \
way to express one",
'0'..='9' => "; backreferences need memory a grammar does not have",
_ => "",
}
)));
}
} else if c == '"' {
literal.push_str("\\\"");
self.pos += 1;
} else if c == '\\' {
return Err(self.refuse("the pattern ends in a dangling '\\'"));
} else if !is_non_literal(c)
&& (self.pos == len - 1
|| literal.is_empty()
|| self.chars[self.pos + 1] == '.'
|| !is_non_literal(self.chars[self.pos + 1]))
{
literal.push(c);
self.pos += 1;
} else {
break;
}
}
Ok(if literal.is_empty() {
None
} else {
Some(Piece::literal(literal))
})
}
fn char_class(&mut self) -> Result<String, SchemaError> {
let len = self.chars.len();
let mut out = String::from("[");
self.pos += 1;
while self.pos < len && self.chars[self.pos] != ']' {
let c = self.chars[self.pos];
if c != '\\' {
out.push(c);
self.pos += 1;
continue;
}
let next = self
.at(self.pos + 1)
.ok_or_else(|| self.refuse("unbalanced square brackets"))?;
match next {
_ if shorthand_members(next).is_some() => {
out.push_str(shorthand_members(next).unwrap_or(""));
}
'\\' | '"' | '[' | ']' | 'n' | 'r' | 't' | 'x' | 'u' | 'U' => {
out.push('\\');
out.push(next);
}
'-' => out.push_str("\\x2D"),
'^' | '$' | '.' | '(' | ')' | '|' | '{' | '}' | '*' | '+' | '?' | '/' => {
out.push(next)
}
_ => {
return Err(self.refuse(format!(
"the escape \"\\{next}\" has no GBNF spelling inside a [] class"
)))
}
}
self.pos += 2;
}
if self.pos >= len {
return Err(self.refuse("unbalanced square brackets"));
}
out.push(']');
self.pos += 1;
Ok(out)
}
fn repetition(&mut self, seq: &mut [Piece]) -> Result<(), SchemaError> {
let len = self.chars.len();
self.pos += 1;
let mut text = String::new();
while self.pos < len && self.chars[self.pos] != '}' {
text.push(self.chars[self.pos]);
self.pos += 1;
}
if self.pos >= len {
return Err(self.refuse("unbalanced curly brackets"));
}
self.pos += 1;
let parse = |part: &str| -> Result<u64, SchemaError> {
part.trim()
.parse::<u64>()
.map_err(|_| self.refuse(format!("{part:?} in {{}} is not a non-negative integer")))
};
let parts: Vec<&str> = text.split(',').collect();
let (min, max) = match parts.as_slice() {
[only] => {
let n = parse(only)?;
(n, Some(n))
}
[lo, hi] => (
if lo.is_empty() { 0 } else { parse(lo)? },
if hi.is_empty() {
None
} else {
Some(parse(hi)?)
},
),
_ => return Err(self.refuse("wrong number of values in curly brackets")),
};
let last = seq
.last()
.ok_or_else(|| self.refuse("'{' has nothing before it to repeat"))?
.clone();
let sub = if last.literal {
format!("\"{}\"", last.text)
} else if let Some(id) = self.sub_rule_ids.get(&last.text) {
id.clone()
} else {
let index = self.sub_rule_ids.len() + 1;
let name = format!("{}-{index}", self.name);
let id = self.conv.add_rule(&name, &last.text);
self.sub_rule_ids.insert(last.text.clone(), id.clone());
id
};
if let Some(slot) = seq.last_mut() {
*slot = Piece::rule(build_repetition(&sub, min, max, ""));
}
Ok(())
}
}