use omgkit_core::ChiralTag;
use super::expr::{AtomExpr, AtomPrim};
use crate::error::{ParseError, ParseErrorKind as K, Result};
pub fn parse_atom_expr(src: &[u8]) -> Result<AtomExpr> {
let mut p = ExprParser { src, pos: 0 };
let e = p.low_and()?;
if p.pos != src.len() {
return Err(ParseError::new(
K::BadBracketAtom("方括号里有无法解析的残余"),
p.pos,
src,
));
}
Ok(e)
}
struct ExprParser<'a> {
src: &'a [u8],
pos: usize,
}
impl ExprParser<'_> {
fn peek(&self) -> Option<u8> {
self.src.get(self.pos).copied()
}
fn eat(&mut self, b: u8) -> bool {
if self.peek() == Some(b) {
self.pos += 1;
true
} else {
false
}
}
fn err<T>(&self, why: &'static str) -> Result<T> {
Err(ParseError::new(K::BadBracketAtom(why), self.pos, self.src))
}
fn low_and(&mut self) -> Result<AtomExpr> {
let mut parts = vec![self.or()?];
while self.eat(b';') {
parts.push(self.or()?);
}
Ok(flatten(AtomExpr::And, parts))
}
fn or(&mut self) -> Result<AtomExpr> {
let mut parts = vec![self.high_and()?];
while self.eat(b',') {
parts.push(self.high_and()?);
}
Ok(flatten(AtomExpr::Or, parts))
}
fn high_and(&mut self) -> Result<AtomExpr> {
let mut parts = vec![self.unary()?];
loop {
if self.eat(b'&') {
parts.push(self.unary()?);
continue;
}
match self.peek() {
Some(b) if starts_primitive(b) => parts.push(self.unary()?),
_ => break,
}
}
Ok(flatten(AtomExpr::And, parts))
}
fn unary(&mut self) -> Result<AtomExpr> {
if self.eat(b'!') {
return Ok(AtomExpr::Not(Box::new(self.unary()?)));
}
self.primitive()
}
fn primitive(&mut self) -> Result<AtomExpr> {
let Some(b) = self.peek() else {
return self.err("表达式意外结束");
};
if b.is_ascii_alphabetic() {
if let Some(prim) = self.try_two_char_element() {
return Ok(AtomExpr::Prim(prim));
}
}
let prim = match b {
b'*' => {
self.pos += 1;
AtomPrim::Any
}
b'a' => {
self.pos += 1;
AtomPrim::Aromatic
}
b'A' => {
self.pos += 1;
AtomPrim::Aliphatic
}
b'#' => {
self.pos += 1;
let z = self.number().ok_or_else(|| {
ParseError::new(K::BadBracketAtom("`#` 后缺少原子序数"), self.pos, self.src)
})?;
AtomPrim::Element {
z: u8::try_from(z)
.map_err(|_| ParseError::new(K::NumberOverflow, self.pos, self.src))?,
aromatic: None,
}
}
b'D' | b'X' | b'H' | b'h' | b'R' | b'r' | b'x' | b'v' => {
self.pos += 1;
let n = self.number();
count_primitive(b, n)
}
b'+' | b'-' => AtomPrim::Charge(self.charge()),
b'@' => {
self.pos += 1;
if self.eat(b'@') {
AtomPrim::Chirality(ChiralTag::Cw)
} else {
AtomPrim::Chirality(ChiralTag::Ccw)
}
}
b':' => {
self.pos += 1;
let n = self.number().ok_or_else(|| {
ParseError::new(K::BadBracketAtom("`:` 后缺少映射号"), self.pos, self.src)
})?;
AtomPrim::AtomMap(u16::try_from(n).unwrap_or(u16::MAX))
}
b'$' => self.recursive()?,
b'0'..=b'9' => {
let n = self.number().expect("已 peek 到数字");
AtomPrim::Isotope(u16::try_from(n).unwrap_or(u16::MAX))
}
_ if b.is_ascii_alphabetic() => self.element()?,
_ => return self.err("无法识别的查询基元"),
};
Ok(AtomExpr::Prim(prim))
}
fn recursive(&mut self) -> Result<AtomPrim> {
let dollar = self.pos;
self.pos += 1;
if !self.eat(b'(') {
return Err(ParseError::new(
K::BadBracketAtom("`$` 后缺少 `(`"),
self.pos,
self.src,
));
}
let start = self.pos;
let mut depth = 0usize;
loop {
match self.peek() {
None => {
return Err(ParseError::new(
K::BadBracketAtom("递归 SMARTS 的 `(` 未闭合"),
dollar,
self.src,
))
}
Some(b'(') => {
depth += 1;
self.pos += 1;
}
Some(b')') if depth == 0 => break,
Some(b')') => {
depth -= 1;
self.pos += 1;
}
Some(_) => self.pos += 1,
}
}
let inner = &self.src[start..self.pos];
self.pos += 1;
let text = std::str::from_utf8(inner)
.map_err(|_| ParseError::new(K::UnexpectedEnd, start, self.src))?;
let sub = super::mol::parse(text)
.map_err(|e| ParseError::new(e.kind, start + e.pos, self.src))?;
Ok(AtomPrim::Recursive(Box::new(sub)))
}
fn try_two_char_element(&mut self) -> Option<AtomPrim> {
let two = self.src.get(self.pos..self.pos + 2)?;
let sym = std::str::from_utf8(two).ok()?;
if let Some(z) = aromatic_two_char(sym) {
self.pos += 2;
return Some(AtomPrim::Element {
z,
aromatic: Some(true),
});
}
if !(two[0].is_ascii_uppercase() && two[1].is_ascii_lowercase()) {
return None;
}
let z = omgkit_core::element::atomic_num_of(sym)?;
self.pos += 2;
Some(AtomPrim::Element {
z,
aromatic: Some(false),
})
}
fn element(&mut self) -> Result<AtomPrim> {
let b = self.src[self.pos];
let upper = char::from(b).to_ascii_uppercase().to_string();
let Some(z) = omgkit_core::element::atomic_num_of(&upper) else {
return Err(ParseError::new(
K::UnknownElement(char::from(b).to_string()),
self.pos,
self.src,
));
};
self.pos += 1;
Ok(AtomPrim::Element {
z,
aromatic: Some(b.is_ascii_lowercase()),
})
}
fn charge(&mut self) -> i32 {
let sign: i32 = if self.peek() == Some(b'+') { 1 } else { -1 };
let ch = if sign > 0 { b'+' } else { b'-' };
self.pos += 1;
let mut n = 1i32;
while self.peek() == Some(ch) {
self.pos += 1;
n += 1;
}
if n == 1 {
if let Some(v) = self.number() {
n = i32::try_from(v).unwrap_or(i32::MAX);
}
}
n * sign
}
fn number(&mut self) -> Option<u32> {
let start = self.pos;
let mut v: u64 = 0;
while let Some(d @ b'0'..=b'9') = self.peek() {
v = (v * 10 + u64::from(d - b'0')).min(u64::from(u32::MAX));
self.pos += 1;
}
if self.pos == start {
None
} else {
u32::try_from(v).ok()
}
}
}
fn flatten(wrap: fn(Vec<AtomExpr>) -> AtomExpr, mut parts: Vec<AtomExpr>) -> AtomExpr {
if parts.len() == 1 {
parts.pop().expect("非空")
} else {
wrap(parts)
}
}
fn count_primitive(letter: u8, n: Option<u32>) -> AtomPrim {
match letter {
b'D' => AtomPrim::Degree(n.unwrap_or(1)),
b'X' => AtomPrim::TotalDegree(n.unwrap_or(1)),
b'v' => AtomPrim::Valence(n.unwrap_or(1)),
b'H' => AtomPrim::TotalHs(n.unwrap_or(1)),
b'h' => AtomPrim::ImplicitHs(n.unwrap_or(1)),
b'R' => AtomPrim::RingCount(n),
b'r' => match n {
Some(k) => AtomPrim::RingSize(Some(k)),
None => AtomPrim::RingCount(None),
},
b'x' => AtomPrim::RingBondCount(n),
_ => unreachable!("由调用方 match 保证"),
}
}
fn starts_primitive(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'*' | b'#' | b'+' | b'-' | b'@' | b':' | b'!' | b'$')
}
fn aromatic_two_char(sym: &str) -> Option<u8> {
match sym {
"se" => Some(34),
"as" => Some(33),
"te" => Some(52),
"si" => Some(14),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> AtomExpr {
parse_atom_expr(s.as_bytes()).unwrap_or_else(|e| panic!("{s}: {}", e.render()))
}
fn elem(z: u8, aromatic: bool) -> AtomExpr {
AtomExpr::Prim(AtomPrim::Element {
z,
aromatic: Some(aromatic),
})
}
fn prim(p: AtomPrim) -> AtomExpr {
AtomExpr::Prim(p)
}
#[test]
fn operator_precedence() {
let c = || elem(6, false);
let n = || elem(7, false);
let h1 = || prim(AtomPrim::TotalHs(1));
assert_eq!(
p("C,N;H1"),
AtomExpr::And(vec![AtomExpr::Or(vec![c(), n()]), h1()])
);
assert_eq!(
p("C,N&H1"),
AtomExpr::Or(vec![c(), AtomExpr::And(vec![n(), h1()])])
);
assert_eq!(p("C,NH1"), p("C,N&H1"));
}
#[test]
fn negation_binds_tightest() {
assert_eq!(
p("!C;!N"),
AtomExpr::And(vec![
AtomExpr::Not(Box::new(elem(6, false))),
AtomExpr::Not(Box::new(elem(7, false))),
])
);
assert_eq!(
p("!C,N"),
AtomExpr::Or(vec![
AtomExpr::Not(Box::new(elem(6, false))),
elem(7, false)
])
);
}
#[test]
fn single_primitive_is_not_wrapped() {
assert_eq!(p("C"), elem(6, false));
assert_eq!(p("c"), elem(6, true));
assert_eq!(
p("#6"),
AtomExpr::Prim(AtomPrim::Element {
z: 6,
aromatic: None
})
);
}
#[test]
fn bare_count_primitives_have_different_defaults() {
assert_eq!(p("D"), prim(AtomPrim::Degree(1)));
assert_eq!(p("X"), prim(AtomPrim::TotalDegree(1)));
assert_eq!(p("v"), prim(AtomPrim::Valence(1)));
assert_eq!(p("R"), prim(AtomPrim::RingCount(None)));
assert_eq!(p("x"), prim(AtomPrim::RingBondCount(None)));
assert_eq!(p("h"), prim(AtomPrim::ImplicitHs(1)));
assert_eq!(p("r"), prim(AtomPrim::RingCount(None)));
assert_eq!(p("r5"), prim(AtomPrim::RingSize(Some(5))));
assert_eq!(p("R2"), prim(AtomPrim::RingCount(Some(2))));
}
#[test]
fn two_char_element_beats_one_char_plus_aromatic() {
assert_eq!(p("Sc"), elem(21, false), "钪");
assert_eq!(p("Cl"), elem(17, false));
assert_eq!(p("Na"), elem(11, false));
assert_eq!(p("Si"), elem(14, false));
assert_eq!(p("Hg"), elem(80, false), "汞,不是 H1 & 芳香碳");
assert_eq!(p("Ho"), elem(67, false), "钬,不是 H1 & 芳香氧");
assert_eq!(p("Rb"), elem(37, false), "铷,不是 R & 芳香硼");
assert_eq!(p("Ac"), elem(89, false), "锕,不是 A & 芳香碳");
assert_eq!(p("se"), elem(34, true));
assert_eq!(p("as"), elem(33, true), "砷,不是 a & 硫");
assert_eq!(p("te"), elem(52, true));
}
#[test]
fn invalid_two_char_falls_back_to_single_char_primitives() {
assert_eq!(
p("aS"),
AtomExpr::And(vec![prim(AtomPrim::Aromatic), elem(16, false)]),
"`aS` 不是符号"
);
assert_eq!(
p("AC"),
AtomExpr::And(vec![prim(AtomPrim::Aliphatic), elem(6, false)]),
"`AC` 不是符号"
);
assert_eq!(
p("ac"),
AtomExpr::And(vec![prim(AtomPrim::Aromatic), elem(6, true)]),
"`ac` 不是符号"
);
assert_eq!(
p("Va"),
AtomExpr::And(vec![elem(23, false), prim(AtomPrim::Aromatic)]),
"没有 `Va` 这个元素,退回钒 & 芳香"
);
assert_eq!(
p("Xx"),
AtomExpr::And(vec![
prim(AtomPrim::TotalDegree(1)),
prim(AtomPrim::RingBondCount(None))
]),
"没有 `Xx` 这个元素,退回 X1 & x"
);
}
#[test]
fn charge_forms() {
assert_eq!(p("+"), prim(AtomPrim::Charge(1)));
assert_eq!(p("++"), prim(AtomPrim::Charge(2)));
assert_eq!(p("+2"), prim(AtomPrim::Charge(2)));
assert_eq!(p("-"), prim(AtomPrim::Charge(-1)));
assert_eq!(p("--"), prim(AtomPrim::Charge(-2)));
assert_eq!(p("-3"), prim(AtomPrim::Charge(-3)));
}
#[test]
fn hydrogen_is_a_count_at_this_level() {
assert_eq!(p("H"), prim(AtomPrim::TotalHs(1)));
assert_eq!(p("H2"), prim(AtomPrim::TotalHs(2)));
assert_eq!(
p("HH"),
AtomExpr::And(vec![prim(AtomPrim::TotalHs(1)), prim(AtomPrim::TotalHs(1))]),
"并置的两个 H 都是计数"
);
}
#[test]
fn isotope_chirality_and_map() {
assert_eq!(
p("13C"),
AtomExpr::And(vec![prim(AtomPrim::Isotope(13)), elem(6, false)])
);
assert_eq!(
p("C@"),
AtomExpr::And(vec![
elem(6, false),
prim(AtomPrim::Chirality(ChiralTag::Ccw))
])
);
assert_eq!(
p("C@@"),
AtomExpr::And(vec![
elem(6, false),
prim(AtomPrim::Chirality(ChiralTag::Cw))
])
);
assert_eq!(
p("C:1"),
AtomExpr::And(vec![elem(6, false), prim(AtomPrim::AtomMap(1))])
);
}
#[test]
fn syntax_errors_have_positions() {
for (s, at) in [("#", 1usize), ("C&", 2), ("C,", 2), (":", 1)] {
let err = parse_atom_expr(s.as_bytes()).expect_err(&format!("{s} 应当解析失败"));
assert_eq!(err.pos, at, "{s} 的报错位置");
}
}
}