Skip to main content

lolli_parse/
lib.rs

1//! # lolli-parse
2//!
3//! Parser for the Lolli linear logic workbench.
4//!
5//! This crate provides parsing functionality for linear logic formulas and sequents.
6//!
7//! ## Supported Syntax
8//!
9//! | Connective | Unicode | ASCII |
10//! |------------|---------|-------|
11//! | Tensor | ⊗ | * |
12//! | Par | ⅋ | \`|\` |
13//! | Lolli | ⊸ | -o |
14//! | With | & | & |
15//! | Plus | ⊕ | + |
16//! | Bang | ! | ! |
17//! | Why not | ? | ? |
18//! | Negation | A⊥ | A^ |
19//! | Turnstile | ⊢ | \|- |
20//!
21//! ## Example
22//!
23//! ```
24//! use lolli_parse::{parse_formula, parse_sequent};
25//!
26//! let formula = parse_formula("A -o B").unwrap();
27//! assert_eq!(formula.pretty(), "(A ⊸ B)");
28//!
29//! let sequent = parse_sequent("A, B |- A * B").unwrap();
30//! assert_eq!(sequent.antecedent.len(), 2);
31//! ```
32
33#![warn(missing_docs)]
34#![warn(clippy::all)]
35
36use pest::Parser;
37use pest_derive::Parser;
38
39pub use lolli_core::{Formula, Sequent, TwoSidedSequent};
40
41#[derive(Parser)]
42#[grammar = "grammar.pest"]
43struct LolliParser;
44
45/// Parse error type.
46#[derive(Debug, thiserror::Error)]
47pub enum ParseError {
48    /// Unexpected token in input
49    #[error("Unexpected token: {0}")]
50    UnexpectedToken(String),
51
52    /// Unknown operator
53    #[error("Unknown operator: {0}")]
54    UnknownOperator(String),
55
56    /// Unexpected rule during parsing
57    #[error("Unexpected rule: {0}")]
58    UnexpectedRule(String),
59
60    /// Pest parsing error
61    #[error("Parse error: {0}")]
62    PestError(#[from] pest::error::Error<Rule>),
63
64    /// Empty input
65    #[error("Empty input")]
66    EmptyInput,
67}
68
69/// Parse a formula from a string.
70///
71/// # Examples
72///
73/// ```
74/// use lolli_parse::parse_formula;
75///
76/// // Simple atom
77/// let f = parse_formula("A").unwrap();
78///
79/// // Linear implication
80/// let f = parse_formula("A -o B").unwrap();
81///
82/// // Complex formula
83/// let f = parse_formula("!A * B -o C + D").unwrap();
84/// ```
85///
86/// # Errors
87///
88/// Returns a `ParseError` if the input is not a valid formula.
89pub fn parse_formula(input: &str) -> Result<Formula, ParseError> {
90    let pairs = LolliParser::parse(Rule::formula, input)?;
91    let pair = pairs.into_iter().next().ok_or(ParseError::EmptyInput)?;
92    build_formula(pair)
93}
94
95/// Parse a sequent from a string.
96///
97/// # Examples
98///
99/// ```
100/// use lolli_parse::parse_sequent;
101///
102/// // Two-sided sequent
103/// let s = parse_sequent("A, B |- C").unwrap();
104///
105/// // One-sided sequent (right side only)
106/// let s = parse_sequent("|- A, B").unwrap();
107/// ```
108///
109/// # Errors
110///
111/// Returns a `ParseError` if the input is not a valid sequent.
112pub fn parse_sequent(input: &str) -> Result<TwoSidedSequent, ParseError> {
113    let pairs = LolliParser::parse(Rule::sequent, input)?;
114    let pair = pairs.into_iter().next().ok_or(ParseError::EmptyInput)?;
115    build_sequent(pair)
116}
117
118use pest::iterators::Pair;
119
120fn build_formula(pair: Pair<Rule>) -> Result<Formula, ParseError> {
121    match pair.as_rule() {
122        Rule::formula => {
123            let inner = pair.into_inner().next().ok_or(ParseError::EmptyInput)?;
124            build_formula(inner)
125        }
126        Rule::lolli_expr => build_lolli_expr(pair),
127        Rule::par_expr => build_left_assoc_binary(pair, Rule::par_op, Formula::par),
128        Rule::tensor_expr => build_left_assoc_binary(pair, Rule::tensor_op, Formula::tensor),
129        Rule::plus_expr => build_left_assoc_binary(pair, Rule::plus_op, Formula::plus),
130        Rule::with_expr => build_left_assoc_binary(pair, Rule::with_op, Formula::with),
131        Rule::unary_expr => build_unary_expr(pair),
132        Rule::primary_expr => build_primary_expr(pair),
133        Rule::ident => Ok(Formula::Atom(pair.as_str().to_string())),
134        Rule::one => Ok(Formula::One),
135        Rule::bottom => Ok(Formula::Bottom),
136        Rule::top => Ok(Formula::Top),
137        Rule::zero => Ok(Formula::Zero),
138        _ => Err(ParseError::UnexpectedRule(format!("{:?}", pair.as_rule()))),
139    }
140}
141
142fn build_lolli_expr(pair: Pair<Rule>) -> Result<Formula, ParseError> {
143    let mut inner = pair.into_inner();
144    let first = inner.next().ok_or(ParseError::EmptyInput)?;
145    let mut result = build_formula(first)?;
146
147    // Check for lolli operator and right side
148    while let Some(op_or_expr) = inner.next() {
149        if op_or_expr.as_rule() == Rule::lolli_op {
150            // Get the right side (which is itself a lolli_expr for right associativity)
151            let right = inner.next().ok_or(ParseError::EmptyInput)?;
152            let right_formula = build_formula(right)?;
153            result = Formula::lolli(result, right_formula);
154        } else {
155            // It's the right side directly
156            let right_formula = build_formula(op_or_expr)?;
157            result = Formula::lolli(result, right_formula);
158        }
159    }
160
161    Ok(result)
162}
163
164fn build_left_assoc_binary<F>(
165    pair: Pair<Rule>,
166    _op_rule: Rule,
167    constructor: F,
168) -> Result<Formula, ParseError>
169where
170    F: Fn(Formula, Formula) -> Formula,
171{
172    let mut inner = pair.into_inner();
173    let first = inner.next().ok_or(ParseError::EmptyInput)?;
174    let mut result = build_formula(first)?;
175
176    while let Some(next) = inner.next() {
177        // Skip operator tokens
178        if next.as_rule() == _op_rule {
179            continue;
180        }
181        let right = build_formula(next)?;
182        result = constructor(result, right);
183    }
184
185    Ok(result)
186}
187
188fn build_unary_expr(pair: Pair<Rule>) -> Result<Formula, ParseError> {
189    let mut inner = pair.into_inner().peekable();
190
191    // Check for prefix operators
192    let first = inner.peek().ok_or(ParseError::EmptyInput)?;
193
194    match first.as_rule() {
195        Rule::bang_op => {
196            inner.next(); // consume the operator
197            let operand = inner.next().ok_or(ParseError::EmptyInput)?;
198            let formula = build_formula(operand)?;
199            Ok(Formula::of_course(formula))
200        }
201        Rule::whynot_op => {
202            inner.next(); // consume the operator
203            let operand = inner.next().ok_or(ParseError::EmptyInput)?;
204            let formula = build_formula(operand)?;
205            Ok(Formula::why_not(formula))
206        }
207        _ => {
208            // Primary expression with optional negation suffix
209            let primary = inner.next().ok_or(ParseError::EmptyInput)?;
210            let mut formula = build_formula(primary)?;
211
212            // Check for negation suffix
213            if let Some(suffix) = inner.next() {
214                if suffix.as_rule() == Rule::negation_suffix {
215                    formula = formula.negate();
216                }
217            }
218
219            Ok(formula)
220        }
221    }
222}
223
224fn build_primary_expr(pair: Pair<Rule>) -> Result<Formula, ParseError> {
225    let inner = pair.into_inner().next().ok_or(ParseError::EmptyInput)?;
226    build_formula(inner)
227}
228
229fn build_sequent(pair: Pair<Rule>) -> Result<TwoSidedSequent, ParseError> {
230    let mut antecedent = Vec::new();
231    let mut succedent = Vec::new();
232    let mut seen_turnstile = false;
233
234    for inner in pair.into_inner() {
235        match inner.as_rule() {
236            Rule::turnstile => {
237                seen_turnstile = true;
238            }
239            Rule::formula_list => {
240                let formulas = build_formula_list(inner)?;
241                if seen_turnstile {
242                    succedent = formulas;
243                } else {
244                    antecedent = formulas;
245                }
246            }
247            _ => {}
248        }
249    }
250
251    Ok(TwoSidedSequent::new(antecedent, succedent))
252}
253
254fn build_formula_list(pair: Pair<Rule>) -> Result<Vec<Formula>, ParseError> {
255    let mut formulas = Vec::new();
256    for inner in pair.into_inner() {
257        if inner.as_rule() == Rule::formula {
258            formulas.push(build_formula(inner)?);
259        }
260    }
261    Ok(formulas)
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_parse_atoms() {
270        let f = parse_formula("A").unwrap();
271        assert_eq!(f, Formula::atom("A"));
272
273        let f = parse_formula("foo").unwrap();
274        assert_eq!(f, Formula::atom("foo"));
275
276        let f = parse_formula("FileHandle").unwrap();
277        assert_eq!(f, Formula::atom("FileHandle"));
278    }
279
280    #[test]
281    fn test_parse_units() {
282        assert_eq!(parse_formula("1").unwrap(), Formula::One);
283        assert_eq!(parse_formula("one").unwrap(), Formula::One);
284        assert_eq!(parse_formula("bot").unwrap(), Formula::Bottom);
285        assert_eq!(parse_formula("bottom").unwrap(), Formula::Bottom);
286        assert_eq!(parse_formula("top").unwrap(), Formula::Top);
287        assert_eq!(parse_formula("0").unwrap(), Formula::Zero);
288        assert_eq!(parse_formula("zero").unwrap(), Formula::Zero);
289    }
290
291    #[test]
292    fn test_parse_negation() {
293        let f = parse_formula("A^").unwrap();
294        assert_eq!(f, Formula::neg_atom("A"));
295
296        let f = parse_formula("A⊥").unwrap();
297        assert_eq!(f, Formula::neg_atom("A"));
298    }
299
300    #[test]
301    fn test_parse_unary() {
302        let f = parse_formula("!A").unwrap();
303        assert_eq!(f, Formula::of_course(Formula::atom("A")));
304
305        let f = parse_formula("?A").unwrap();
306        assert_eq!(f, Formula::why_not(Formula::atom("A")));
307
308        let f = parse_formula("!!A").unwrap();
309        assert_eq!(
310            f,
311            Formula::of_course(Formula::of_course(Formula::atom("A")))
312        );
313    }
314
315    #[test]
316    fn test_parse_lolli() {
317        let f = parse_formula("A -o B").unwrap();
318        assert_eq!(f, Formula::lolli(Formula::atom("A"), Formula::atom("B")));
319
320        // Right associativity: A -o B -o C = A -o (B -o C)
321        let f = parse_formula("A -o B -o C").unwrap();
322        assert_eq!(
323            f,
324            Formula::lolli(
325                Formula::atom("A"),
326                Formula::lolli(Formula::atom("B"), Formula::atom("C"))
327            )
328        );
329    }
330
331    #[test]
332    fn test_parse_tensor() {
333        let f = parse_formula("A * B").unwrap();
334        assert_eq!(f, Formula::tensor(Formula::atom("A"), Formula::atom("B")));
335
336        // Left associativity: A * B * C = (A * B) * C
337        let f = parse_formula("A * B * C").unwrap();
338        assert_eq!(
339            f,
340            Formula::tensor(
341                Formula::tensor(Formula::atom("A"), Formula::atom("B")),
342                Formula::atom("C")
343            )
344        );
345    }
346
347    #[test]
348    fn test_parse_plus() {
349        let f = parse_formula("A + B").unwrap();
350        assert_eq!(f, Formula::plus(Formula::atom("A"), Formula::atom("B")));
351    }
352
353    #[test]
354    fn test_parse_with() {
355        let f = parse_formula("A & B").unwrap();
356        assert_eq!(f, Formula::with(Formula::atom("A"), Formula::atom("B")));
357    }
358
359    #[test]
360    fn test_parse_parentheses() {
361        let f = parse_formula("(A -o B)").unwrap();
362        assert_eq!(f, Formula::lolli(Formula::atom("A"), Formula::atom("B")));
363
364        // Override precedence
365        let f = parse_formula("(A + B) * C").unwrap();
366        assert_eq!(
367            f,
368            Formula::tensor(
369                Formula::plus(Formula::atom("A"), Formula::atom("B")),
370                Formula::atom("C")
371            )
372        );
373    }
374
375    #[test]
376    fn test_parse_precedence() {
377        // * binds tighter than -o
378        // A * B -o C = (A * B) -o C
379        let f = parse_formula("A * B -o C").unwrap();
380        assert_eq!(
381            f,
382            Formula::lolli(
383                Formula::tensor(Formula::atom("A"), Formula::atom("B")),
384                Formula::atom("C")
385            )
386        );
387
388        // In our grammar (following the hierarchy):
389        // lolli < par < tensor < plus < with < unary
390        // So * binds tighter than +
391        // A + B * C = (A + B) * C
392        let f = parse_formula("A + B * C").unwrap();
393        assert_eq!(
394            f,
395            Formula::tensor(
396                Formula::plus(Formula::atom("A"), Formula::atom("B")),
397                Formula::atom("C")
398            )
399        );
400
401        // & binds tightest
402        // A & B + C = (A & B) + C
403        let f = parse_formula("A & B + C").unwrap();
404        assert_eq!(
405            f,
406            Formula::plus(
407                Formula::with(Formula::atom("A"), Formula::atom("B")),
408                Formula::atom("C")
409            )
410        );
411    }
412
413    #[test]
414    fn test_parse_complex() {
415        let f = parse_formula("!A * B -o ?C + D").unwrap();
416        // !A * B = tensor(!A, B)
417        // ?C + D = plus(?C, D)
418        // result = lolli(tensor(!A, B), plus(?C, D))
419        assert_eq!(
420            f,
421            Formula::lolli(
422                Formula::tensor(
423                    Formula::of_course(Formula::atom("A")),
424                    Formula::atom("B")
425                ),
426                Formula::plus(Formula::why_not(Formula::atom("C")), Formula::atom("D"))
427            )
428        );
429    }
430
431    #[test]
432    fn test_parse_sequent() {
433        let s = parse_sequent("A, B |- C").unwrap();
434        assert_eq!(s.antecedent.len(), 2);
435        assert_eq!(s.succedent.len(), 1);
436        assert_eq!(s.antecedent[0], Formula::atom("A"));
437        assert_eq!(s.antecedent[1], Formula::atom("B"));
438        assert_eq!(s.succedent[0], Formula::atom("C"));
439    }
440
441    #[test]
442    fn test_parse_sequent_one_sided() {
443        let s = parse_sequent("|- A, B").unwrap();
444        assert_eq!(s.antecedent.len(), 0);
445        assert_eq!(s.succedent.len(), 2);
446    }
447
448    #[test]
449    fn test_parse_sequent_complex() {
450        let s = parse_sequent("A * B, C -o D |- E + F").unwrap();
451        assert_eq!(s.antecedent.len(), 2);
452        assert_eq!(s.succedent.len(), 1);
453    }
454
455    #[test]
456    fn test_roundtrip() {
457        let formulas = vec![
458            "A",
459            "A -o B",
460            "A * B",
461            "A + B",
462            "A & B",
463            "!A",
464            "?A",
465            "1",
466            "bot",
467            "top",
468            "0",
469            "A * B -o C",
470            "!A * ?B -o C + D",
471        ];
472
473        for input in formulas {
474            let f = parse_formula(input).unwrap();
475            // Just verify it parses without error
476            let _ = f.pretty();
477        }
478    }
479}