logicaffeine_proof/
formula.rs1use crate::{ProofExpr, ProofTerm};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct FormulaError {
36 pub message: String,
37}
38
39impl FormulaError {
40 pub(crate) fn new(message: impl Into<String>) -> Self {
41 FormulaError { message: message.into() }
42 }
43}
44
45impl std::fmt::Display for FormulaError {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "formula parse error: {}", self.message)
48 }
49}
50
51impl std::error::Error for FormulaError {}
52
53pub fn parse_formula(input: &str) -> Result<ProofExpr, FormulaError> {
58 let tokens = tokenize(input)?;
59 let mut p = Parser { tokens: &tokens, pos: 0 };
60 let expr = p.parse_formula()?;
61 if p.pos != p.tokens.len() {
62 return Err(FormulaError::new(format!(
63 "unexpected trailing input near token {:?}",
64 p.tokens.get(p.pos)
65 )));
66 }
67 Ok(expr)
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
75enum Tok {
76 Ident(String),
77 LParen,
78 RParen,
79 Comma,
80 Eq,
81 Arrow, Iff, And, Or, Not, Forall, Exists, }
89
90fn tokenize(input: &str) -> Result<Vec<Tok>, FormulaError> {
91 let chars: Vec<char> = input.chars().collect();
92 let mut toks = Vec::new();
93 let mut i = 0;
94 while i < chars.len() {
95 let c = chars[i];
96 if c.is_whitespace() {
97 i += 1;
98 continue;
99 }
100 match c {
101 '(' => {
102 toks.push(Tok::LParen);
103 i += 1;
104 }
105 ')' => {
106 toks.push(Tok::RParen);
107 i += 1;
108 }
109 ',' => {
110 toks.push(Tok::Comma);
111 i += 1;
112 }
113 '.' | ';' => {
116 i += 1;
117 }
118 '=' => {
119 toks.push(Tok::Eq);
120 i += 1;
121 }
122 '∧' => {
123 toks.push(Tok::And);
124 i += 1;
125 }
126 '∨' => {
127 toks.push(Tok::Or);
128 i += 1;
129 }
130 '¬' => {
131 toks.push(Tok::Not);
132 i += 1;
133 }
134 '∀' => {
135 toks.push(Tok::Forall);
136 i += 1;
137 }
138 '∃' => {
139 toks.push(Tok::Exists);
140 i += 1;
141 }
142 '→' => {
143 toks.push(Tok::Arrow);
144 i += 1;
145 }
146 '↔' => {
147 toks.push(Tok::Iff);
148 i += 1;
149 }
150 '-' if i + 1 < chars.len() && chars[i + 1] == '>' => {
151 toks.push(Tok::Arrow);
152 i += 2;
153 }
154 '<' if i + 2 < chars.len() && chars[i + 1] == '-' && chars[i + 2] == '>' => {
155 toks.push(Tok::Iff);
156 i += 3;
157 }
158 c if c.is_alphanumeric() || c == '_' => {
159 let start = i;
160 while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
161 i += 1;
162 }
163 toks.push(Tok::Ident(chars[start..i].iter().collect()));
164 }
165 other => {
166 return Err(FormulaError::new(format!("unexpected character '{other}'")));
167 }
168 }
169 }
170 Ok(toks)
171}
172
173struct Parser<'a> {
178 tokens: &'a [Tok],
179 pos: usize,
180}
181
182impl<'a> Parser<'a> {
183 fn peek(&self) -> Option<&Tok> {
184 self.tokens.get(self.pos)
185 }
186
187 fn advance(&mut self) -> Option<&Tok> {
188 let t = self.tokens.get(self.pos);
189 if t.is_some() {
190 self.pos += 1;
191 }
192 t
193 }
194
195 fn peek_kw(&self) -> Option<String> {
197 match self.peek() {
198 Some(Tok::Ident(s)) => Some(s.to_lowercase()),
199 _ => None,
200 }
201 }
202
203 fn eat_kw(&mut self, kw: &str) -> bool {
204 if self.peek_kw().as_deref() == Some(kw) {
205 self.pos += 1;
206 true
207 } else {
208 false
209 }
210 }
211
212 fn peek_is_quantifier(&self) -> bool {
217 matches!(self.peek(), Some(Tok::Forall) | Some(Tok::Exists))
218 || matches!(
219 self.peek_kw().as_deref(),
220 Some("forall") | Some("for") | Some("exists") | Some("there") | Some("some")
221 )
222 }
223
224 fn parse_formula(&mut self) -> Result<ProofExpr, FormulaError> {
226 if matches!(self.peek(), Some(Tok::Forall)) {
230 self.advance();
231 return self.finish_quantifier(true);
232 }
233 if matches!(self.peek(), Some(Tok::Exists)) {
234 self.advance();
235 return self.finish_quantifier(false);
236 }
237 if self.eat_kw("forall") {
238 return self.finish_quantifier(true);
239 }
240 if self.eat_kw("for") {
241 if !self.eat_kw("all") {
242 return Err(FormulaError::new("expected 'all' after 'for'"));
243 }
244 return self.finish_quantifier(true);
245 }
246 if self.eat_kw("exists") {
247 return self.finish_quantifier(false);
248 }
249 if self.eat_kw("there") {
250 if !(self.eat_kw("exists") || self.eat_kw("is") || self.eat_kw("are")) {
251 return Err(FormulaError::new("expected 'exists' after 'there'"));
252 }
253 return self.finish_quantifier(false);
254 }
255 if self.eat_kw("some") {
256 return self.finish_quantifier(false);
257 }
258 self.parse_iff()
259 }
260
261 fn finish_quantifier(&mut self, universal: bool) -> Result<ProofExpr, FormulaError> {
264 let mut vars = Vec::new();
265 while let Some(Tok::Ident(s)) = self.peek() {
266 let low = s.to_lowercase();
268 if matches!(low.as_str(), "if" | "not" | "true" | "false") {
269 break;
270 }
271 vars.push(s.clone());
272 self.advance();
273 }
274 if vars.is_empty() {
275 return Err(FormulaError::new("quantifier with no bound variables"));
276 }
277 if matches!(self.peek(), Some(Tok::Comma)) {
279 self.advance();
280 }
281 let body = self.parse_formula()?;
282 Ok(vars.into_iter().rev().fold(body, |acc, var| {
283 if universal {
284 ProofExpr::ForAll { variable: var, body: Box::new(acc) }
285 } else {
286 ProofExpr::Exists { variable: var, body: Box::new(acc) }
287 }
288 }))
289 }
290
291 fn parse_iff(&mut self) -> Result<ProofExpr, FormulaError> {
293 let left = self.parse_implication()?;
294 if matches!(self.peek(), Some(Tok::Iff)) || self.peek_kw().as_deref() == Some("iff") {
295 self.advance();
296 let right = self.parse_implication()?;
297 return Ok(ProofExpr::Iff(Box::new(left), Box::new(right)));
298 }
299 Ok(left)
300 }
301
302 fn parse_implication(&mut self) -> Result<ProofExpr, FormulaError> {
305 if self.eat_kw("if") {
306 let cond = self.parse_disjunction()?;
307 if !self.eat_kw("then") {
308 return Err(FormulaError::new("expected 'then' to close an 'if'"));
309 }
310 let conseq = self.parse_implication()?;
311 return Ok(ProofExpr::Implies(Box::new(cond), Box::new(conseq)));
312 }
313 let left = self.parse_disjunction()?;
314 if matches!(self.peek(), Some(Tok::Arrow)) || self.peek_kw().as_deref() == Some("implies") {
315 self.advance();
316 let right = self.parse_implication()?;
317 return Ok(ProofExpr::Implies(Box::new(left), Box::new(right)));
318 }
319 Ok(left)
320 }
321
322 fn parse_disjunction(&mut self) -> Result<ProofExpr, FormulaError> {
324 let mut left = self.parse_conjunction()?;
325 loop {
326 if matches!(self.peek(), Some(Tok::Or)) || self.peek_kw().as_deref() == Some("or") {
327 self.advance();
328 let right = self.parse_conjunction()?;
329 left = ProofExpr::Or(Box::new(left), Box::new(right));
330 } else {
331 break;
332 }
333 }
334 Ok(left)
335 }
336
337 fn parse_conjunction(&mut self) -> Result<ProofExpr, FormulaError> {
339 let mut left = self.parse_negation()?;
340 loop {
341 if matches!(self.peek(), Some(Tok::And)) || self.peek_kw().as_deref() == Some("and") {
342 self.advance();
343 let right = self.parse_negation()?;
344 left = ProofExpr::And(Box::new(left), Box::new(right));
345 } else {
346 break;
347 }
348 }
349 Ok(left)
350 }
351
352 fn parse_negation(&mut self) -> Result<ProofExpr, FormulaError> {
354 if matches!(self.peek(), Some(Tok::Not)) || self.peek_kw().as_deref() == Some("not") {
355 self.advance();
356 let inner = self.parse_negation()?;
357 return Ok(ProofExpr::Not(Box::new(inner)));
358 }
359 self.parse_atom()
360 }
361
362 fn parse_atom(&mut self) -> Result<ProofExpr, FormulaError> {
368 if self.peek_is_quantifier() {
371 return self.parse_formula();
372 }
373
374 if matches!(self.peek(), Some(Tok::LParen)) {
375 self.advance();
376 let inner = self.parse_formula()?;
377 if !matches!(self.advance(), Some(Tok::RParen)) {
378 return Err(FormulaError::new("expected ')'"));
379 }
380 return Ok(inner);
381 }
382
383 let name = match self.advance() {
384 Some(Tok::Ident(s)) => s.clone(),
385 other => {
386 return Err(FormulaError::new(format!(
387 "expected an atom, found {other:?}"
388 )))
389 }
390 };
391
392 match name.to_lowercase().as_str() {
393 "true" => return Ok(ProofExpr::Atom("True".to_string())),
394 "false" => return Ok(ProofExpr::Atom("False".to_string())),
395 _ => {}
396 }
397
398 if matches!(self.peek(), Some(Tok::LParen)) {
400 self.advance();
401 let mut args = Vec::new();
402 if !matches!(self.peek(), Some(Tok::RParen)) {
403 loop {
404 args.push(self.parse_term()?);
405 if matches!(self.peek(), Some(Tok::Comma)) {
406 self.advance();
407 continue;
408 }
409 break;
410 }
411 }
412 if !matches!(self.advance(), Some(Tok::RParen)) {
413 return Err(FormulaError::new("expected ')' to close a relation's arguments"));
414 }
415 return Ok(ProofExpr::Predicate { name, args, world: None });
416 }
417
418 if matches!(self.peek(), Some(Tok::Eq)) {
420 self.advance();
421 let rhs = self.parse_term()?;
422 return Ok(ProofExpr::Identity(ident_to_term(&name), rhs));
423 }
424
425 Ok(ProofExpr::Atom(name))
427 }
428
429 fn parse_term(&mut self) -> Result<ProofTerm, FormulaError> {
430 match self.advance() {
431 Some(Tok::Ident(s)) => Ok(ident_to_term(s)),
432 other => Err(FormulaError::new(format!("expected a term, found {other:?}"))),
433 }
434 }
435}
436
437fn ident_to_term(s: &str) -> ProofTerm {
440 let leads_constant = s
441 .chars()
442 .next()
443 .map(|c| c.is_uppercase() || c.is_ascii_digit())
444 .unwrap_or(false);
445 if leads_constant {
446 ProofTerm::Constant(s.to_string())
447 } else {
448 ProofTerm::Variable(s.to_string())
449 }
450}