logicaffeine_language/
formatter.rs

1//! Output formatters for logical expressions.
2//!
3//! This module defines the [`LogicFormatter`] trait and its implementations for
4//! rendering logical expressions in different notations:
5//!
6//! - **Unicode**: Standard FOL symbols (∀, ∃, ∧, ∨, ¬, →)
7//! - **LaTeX**: TeX math mode notation (\\forall, \\exists, \\land, etc.)
8//! - **ASCII**: Plain text fallback (ALL, EXISTS, AND, OR, NOT)
9//!
10//! Formatters handle quantifiers, connectives, modal operators, temporal operators,
11//! and special constructs like plurals and possessives.
12
13use std::fmt::Write;
14
15use crate::ast::{AspectOperator, ModalDomain, QuantifierKind, TemporalOperator, Term, VoiceOperator};
16use logicaffeine_base::Interner;
17use crate::registry::SymbolRegistry;
18use crate::token::TokenType;
19
20/// Trait for formatting logical expressions in various notations.
21pub trait LogicFormatter {
22    // Quantifiers
23    fn quantifier(&self, kind: &QuantifierKind, var: &str, body: &str) -> String {
24        let sym = match kind {
25            QuantifierKind::Universal => self.universal(),
26            QuantifierKind::Existential => self.existential(),
27            QuantifierKind::Most => "MOST ".to_string(),
28            QuantifierKind::Few => "FEW ".to_string(),
29            QuantifierKind::Many => "MANY ".to_string(),
30            QuantifierKind::Cardinal(n) => self.cardinal(*n),
31            QuantifierKind::AtLeast(n) => self.at_least(*n),
32            QuantifierKind::AtMost(n) => self.at_most(*n),
33            QuantifierKind::Generic => "Gen ".to_string(),
34        };
35        format!("{}{}({})", sym, var, body)
36    }
37
38    fn universal(&self) -> String;
39    fn existential(&self) -> String;
40    fn cardinal(&self, n: u32) -> String;
41    fn at_least(&self, n: u32) -> String;
42    fn at_most(&self, n: u32) -> String;
43
44    // Binary operators
45    fn binary_op(&self, op: &TokenType, left: &str, right: &str) -> String {
46        match op {
47            TokenType::And => format!("({} {} {})", left, self.and(), right),
48            TokenType::Or => format!("({} {} {})", left, self.or(), right),
49            TokenType::If => format!("({} {} {})", left, self.implies(), right),
50            TokenType::Iff => format!("({} {} {})", left, self.iff(), right),
51            _ => String::new(),
52        }
53    }
54
55    fn and(&self) -> &'static str;
56    fn or(&self) -> &'static str;
57    fn implies(&self) -> &'static str;
58    fn iff(&self) -> &'static str;
59
60    // Unary operators
61    fn unary_op(&self, op: &TokenType, operand: &str) -> String {
62        match op {
63            TokenType::Not => format!("{}{}", self.not(), operand),
64            _ => String::new(),
65        }
66    }
67
68    fn not(&self) -> &'static str;
69
70    // Identity operator (used in Identity expressions)
71    fn identity(&self) -> &'static str {
72        " = "
73    }
74
75    // Whether to wrap identity expressions in parentheses
76    fn wrap_identity(&self) -> bool {
77        false
78    }
79
80    // Modal operators
81    fn modal(&self, domain: ModalDomain, force: f32, body: &str) -> String {
82        let sym = match domain {
83            ModalDomain::Alethic if force > 0.0 && force <= 0.5 => self.possibility(),
84            ModalDomain::Alethic => self.necessity(),
85            ModalDomain::Deontic if force <= 0.5 => "P",
86            ModalDomain::Deontic => "O",
87        };
88        format!("{}_{{{:.1}}} {}", sym, force, body)
89    }
90
91    fn necessity(&self) -> &'static str;
92    fn possibility(&self) -> &'static str;
93
94    // Temporal operators
95    fn temporal(&self, op: &TemporalOperator, body: &str) -> String {
96        let sym = match op {
97            TemporalOperator::Past => self.past(),
98            TemporalOperator::Future => self.future(),
99        };
100        format!("{}({})", sym, body)
101    }
102
103    fn past(&self) -> &'static str;
104    fn future(&self) -> &'static str;
105
106    // Aspectual operators
107    fn aspectual(&self, op: &AspectOperator, body: &str) -> String {
108        let sym = match op {
109            AspectOperator::Progressive => self.progressive(),
110            AspectOperator::Perfect => self.perfect(),
111            AspectOperator::Habitual => self.habitual(),
112            AspectOperator::Iterative => self.iterative(),
113        };
114        format!("{}({})", sym, body)
115    }
116
117    fn progressive(&self) -> &'static str;
118    fn perfect(&self) -> &'static str;
119    fn habitual(&self) -> &'static str;
120    fn iterative(&self) -> &'static str;
121
122    // Voice operators
123    fn voice(&self, op: &VoiceOperator, body: &str) -> String {
124        let sym = match op {
125            VoiceOperator::Passive => self.passive(),
126        };
127        format!("{}({})", sym, body)
128    }
129
130    fn passive(&self) -> &'static str;
131
132    // Lambda
133    fn lambda(&self, var: &str, body: &str) -> String;
134
135    // Counterfactual
136    fn counterfactual(&self, antecedent: &str, consequent: &str) -> String;
137
138    // Superlative expansion
139    fn superlative(&self, comp: &str, domain: &str, subject: &str) -> String;
140
141    // Event quantification (uses existential + and)
142    fn event_quantifier(&self, pred: &str, adverbs: &[String]) -> String {
143        if adverbs.is_empty() {
144            format!("{}e({})", self.existential(), pred)
145        } else {
146            let conj = self.and();
147            format!(
148                "{}e({} {} {})",
149                self.existential(),
150                pred,
151                conj,
152                adverbs.join(&format!(" {} ", conj))
153            )
154        }
155    }
156
157    // Categorical (legacy)
158    fn categorical_all(&self) -> &'static str;
159    fn categorical_no(&self) -> &'static str;
160    fn categorical_some(&self) -> &'static str;
161    fn categorical_not(&self) -> &'static str;
162
163    // Sanitization hook for LaTeX special characters
164    fn sanitize(&self, s: &str) -> String {
165        s.to_string()
166    }
167
168    // Whether to use simple predicate form instead of event semantics
169    fn use_simple_events(&self) -> bool {
170        false
171    }
172
173    // Whether to use full predicate names instead of abbreviations
174    fn use_full_names(&self) -> bool {
175        false
176    }
177
178    // Whether to preserve original case (for code generation)
179    fn preserve_case(&self) -> bool {
180        false
181    }
182
183    // Whether to include world arguments in predicates (for Kripke semantics)
184    fn include_world_arguments(&self) -> bool {
185        false
186    }
187
188    /// Hook for customizing how comparatives are rendered.
189    /// Default implementation uses standard logic notation: tallER(subj, obj) or tallER(subj, obj, diff)
190    fn write_comparative<W: Write>(
191        &self,
192        w: &mut W,
193        adjective: &str,
194        subject: &str,
195        object: &str,
196        difference: Option<&str>,
197    ) -> std::fmt::Result {
198        if let Some(diff) = difference {
199            write!(w, "{}er({}, {}, {})", adjective, subject, object, diff)
200        } else {
201            write!(w, "{}er({}, {})", adjective, subject, object)
202        }
203    }
204
205    /// Hook for customizing how predicates are rendered.
206    /// Default implementation uses standard logic notation: Name(Arg1, Arg2)
207    fn write_predicate<W: Write>(
208        &self,
209        w: &mut W,
210        name: &str,
211        args: &[Term],
212        registry: &mut SymbolRegistry,
213        interner: &Interner,
214    ) -> std::fmt::Result {
215        write!(w, "{}(", self.sanitize(name))?;
216        for (i, arg) in args.iter().enumerate() {
217            if i > 0 {
218                write!(w, ", ")?;
219            }
220            if self.use_full_names() {
221                arg.write_to_full(w, registry, interner)?;
222            } else {
223                arg.write_to(w, registry, interner)?;
224            }
225        }
226        write!(w, ")")
227    }
228}
229
230pub struct UnicodeFormatter;
231
232impl LogicFormatter for UnicodeFormatter {
233    fn universal(&self) -> String { "∀".to_string() }
234    fn existential(&self) -> String { "∃".to_string() }
235    fn cardinal(&self, n: u32) -> String { format!("∃={}.", n) }
236    fn at_least(&self, n: u32) -> String { format!("∃≥{}", n) }
237    fn at_most(&self, n: u32) -> String { format!("∃≤{}", n) }
238
239    fn and(&self) -> &'static str { "∧" }
240    fn or(&self) -> &'static str { "∨" }
241    fn implies(&self) -> &'static str { "→" }
242    fn iff(&self) -> &'static str { "↔" }
243    fn not(&self) -> &'static str { "¬" }
244
245    fn necessity(&self) -> &'static str { "□" }
246    fn possibility(&self) -> &'static str { "◇" }
247
248    fn past(&self) -> &'static str { "P" }
249    fn future(&self) -> &'static str { "F" }
250
251    fn progressive(&self) -> &'static str { "Prog" }
252    fn perfect(&self) -> &'static str { "Perf" }
253    fn habitual(&self) -> &'static str { "HAB" }
254    fn iterative(&self) -> &'static str { "ITER" }
255    fn passive(&self) -> &'static str { "Pass" }
256
257    fn lambda(&self, var: &str, body: &str) -> String {
258        format!("λ{}.{}", var, body)
259    }
260
261    fn counterfactual(&self, antecedent: &str, consequent: &str) -> String {
262        format!("({} □→ {})", antecedent, consequent)
263    }
264
265    fn superlative(&self, comp: &str, domain: &str, subject: &str) -> String {
266        format!(
267            "∀x(({}(x) ∧ x ≠ {}) → {}({}, x))",
268            domain, subject, comp, subject
269        )
270    }
271
272    fn categorical_all(&self) -> &'static str { "∀" }
273    fn categorical_no(&self) -> &'static str { "∀¬" }
274    fn categorical_some(&self) -> &'static str { "∃" }
275    fn categorical_not(&self) -> &'static str { "¬" }
276
277    // Use full predicate names (e.g., "Wet" not "W")
278    fn use_full_names(&self) -> bool { true }
279}
280
281pub struct LatexFormatter;
282
283impl LogicFormatter for LatexFormatter {
284    fn universal(&self) -> String { "\\forall ".to_string() }
285    fn existential(&self) -> String { "\\exists ".to_string() }
286    fn cardinal(&self, n: u32) -> String { format!("\\exists_{{={}}} ", n) }
287    fn at_least(&self, n: u32) -> String { format!("\\exists_{{\\geq {}}} ", n) }
288    fn at_most(&self, n: u32) -> String { format!("\\exists_{{\\leq {}}} ", n) }
289
290    fn and(&self) -> &'static str { "\\cdot" }
291    fn or(&self) -> &'static str { "\\vee" }
292    fn implies(&self) -> &'static str { "\\supset" }
293    fn iff(&self) -> &'static str { "\\equiv" }
294    fn not(&self) -> &'static str { "\\sim " }
295
296    fn necessity(&self) -> &'static str { "\\Box" }
297    fn possibility(&self) -> &'static str { "\\Diamond" }
298
299    fn past(&self) -> &'static str { "\\mathsf{P}" }
300    fn future(&self) -> &'static str { "\\mathsf{F}" }
301
302    fn progressive(&self) -> &'static str { "\\mathsf{Prog}" }
303    fn perfect(&self) -> &'static str { "\\mathsf{Perf}" }
304    fn habitual(&self) -> &'static str { "\\mathsf{HAB}" }
305    fn iterative(&self) -> &'static str { "\\mathsf{ITER}" }
306    fn passive(&self) -> &'static str { "\\mathsf{Pass}" }
307
308    fn lambda(&self, var: &str, body: &str) -> String {
309        format!("\\lambda {}.{}", var, body)
310    }
311
312    fn counterfactual(&self, antecedent: &str, consequent: &str) -> String {
313        format!("({} \\boxright {})", antecedent, consequent)
314    }
315
316    fn superlative(&self, comp: &str, domain: &str, subject: &str) -> String {
317        format!(
318            "\\forall x(({}(x) \\land x \\neq {}) \\supset {}({}, x))",
319            domain, subject, comp, subject
320        )
321    }
322
323    fn categorical_all(&self) -> &'static str { "All" }
324    fn categorical_no(&self) -> &'static str { "No" }
325    fn categorical_some(&self) -> &'static str { "Some" }
326    fn categorical_not(&self) -> &'static str { "not" }
327
328    fn sanitize(&self, s: &str) -> String {
329        s.replace('_', r"\_")
330            .replace('^', r"\^{}")
331            .replace('&', r"\&")
332            .replace('%', r"\%")
333            .replace('#', r"\#")
334            .replace('$', r"\$")
335    }
336}
337
338pub struct SimpleFOLFormatter;
339
340impl LogicFormatter for SimpleFOLFormatter {
341    fn universal(&self) -> String { "∀".to_string() }
342    fn existential(&self) -> String { "∃".to_string() }
343    fn cardinal(&self, n: u32) -> String { format!("∃={}", n) }
344    fn at_least(&self, n: u32) -> String { format!("∃≥{}", n) }
345    fn at_most(&self, n: u32) -> String { format!("∃≤{}", n) }
346
347    fn and(&self) -> &'static str { "∧" }
348    fn or(&self) -> &'static str { "∨" }
349    fn implies(&self) -> &'static str { "→" }
350    fn iff(&self) -> &'static str { "↔" }
351    fn not(&self) -> &'static str { "¬" }
352
353    fn necessity(&self) -> &'static str { "□" }
354    fn possibility(&self) -> &'static str { "◇" }
355
356    fn past(&self) -> &'static str { "Past" }
357    fn future(&self) -> &'static str { "Future" }
358
359    fn progressive(&self) -> &'static str { "" }
360    fn perfect(&self) -> &'static str { "" }
361    fn habitual(&self) -> &'static str { "" }
362    fn iterative(&self) -> &'static str { "" }
363    fn passive(&self) -> &'static str { "" }
364
365    fn lambda(&self, var: &str, body: &str) -> String {
366        format!("λ{}.{}", var, body)
367    }
368
369    fn counterfactual(&self, antecedent: &str, consequent: &str) -> String {
370        format!("({} □→ {})", antecedent, consequent)
371    }
372
373    fn superlative(&self, comp: &str, domain: &str, subject: &str) -> String {
374        format!(
375            "∀x(({}(x) ∧ x ≠ {}) → {}({}, x))",
376            domain, subject, comp, subject
377        )
378    }
379
380    fn categorical_all(&self) -> &'static str { "∀" }
381    fn categorical_no(&self) -> &'static str { "∀¬" }
382    fn categorical_some(&self) -> &'static str { "∃" }
383    fn categorical_not(&self) -> &'static str { "¬" }
384
385    fn modal(&self, _domain: ModalDomain, _force: f32, body: &str) -> String {
386        body.to_string()
387    }
388
389    fn aspectual(&self, _op: &AspectOperator, body: &str) -> String {
390        body.to_string()
391    }
392
393    fn use_simple_events(&self) -> bool {
394        true
395    }
396
397    fn use_full_names(&self) -> bool {
398        true
399    }
400}
401
402/// Formatter for Kripke lowered output with explicit world arguments.
403/// Modals are already lowered to quantifiers; this formatter just renders
404/// the result with world arguments appended to predicates.
405pub struct KripkeFormatter;
406
407impl LogicFormatter for KripkeFormatter {
408    fn universal(&self) -> String { "ForAll ".to_string() }
409    fn existential(&self) -> String { "Exists ".to_string() }
410    fn cardinal(&self, n: u32) -> String { format!("Exists={} ", n) }
411    fn at_least(&self, n: u32) -> String { format!("Exists>={} ", n) }
412    fn at_most(&self, n: u32) -> String { format!("Exists<={} ", n) }
413
414    fn and(&self) -> &'static str { " And " }
415    fn or(&self) -> &'static str { " Or " }
416    fn implies(&self) -> &'static str { " Implies " }
417    fn iff(&self) -> &'static str { " Iff " }
418    fn not(&self) -> &'static str { "Not " }
419
420    fn necessity(&self) -> &'static str { "Box" }
421    fn possibility(&self) -> &'static str { "Diamond" }
422
423    fn past(&self) -> &'static str { "Past" }
424    fn future(&self) -> &'static str { "Future" }
425
426    fn progressive(&self) -> &'static str { "Prog" }
427    fn perfect(&self) -> &'static str { "Perf" }
428    fn habitual(&self) -> &'static str { "HAB" }
429    fn iterative(&self) -> &'static str { "ITER" }
430    fn passive(&self) -> &'static str { "Pass" }
431
432    fn lambda(&self, var: &str, body: &str) -> String {
433        format!("Lambda {}.{}", var, body)
434    }
435
436    fn counterfactual(&self, antecedent: &str, consequent: &str) -> String {
437        format!("({} Counterfactual {})", antecedent, consequent)
438    }
439
440    fn superlative(&self, comp: &str, domain: &str, subject: &str) -> String {
441        format!(
442            "ForAll x(({}(x) And x != {}) Implies {}({}, x))",
443            domain, subject, comp, subject
444        )
445    }
446
447    fn categorical_all(&self) -> &'static str { "ForAll" }
448    fn categorical_no(&self) -> &'static str { "ForAll Not" }
449    fn categorical_some(&self) -> &'static str { "Exists" }
450    fn categorical_not(&self) -> &'static str { "Not" }
451
452    fn modal(&self, _domain: ModalDomain, _force: f32, body: &str) -> String {
453        // Modals already lowered to quantifiers - just pass through
454        body.to_string()
455    }
456
457    fn use_full_names(&self) -> bool { true }
458
459    fn include_world_arguments(&self) -> bool { true }
460}
461
462/// Formatter that produces Rust boolean expressions for runtime assertions.
463/// Used by codegen to convert LogicExpr into debug_assert!() compatible code.
464pub struct RustFormatter;
465
466impl LogicFormatter for RustFormatter {
467    // Operators map to Rust boolean operators
468    fn and(&self) -> &'static str { "&&" }
469    fn or(&self) -> &'static str { "||" }
470    fn not(&self) -> &'static str { "!" }
471    fn implies(&self) -> &'static str { "||" } // Handled via binary_op override
472    fn iff(&self) -> &'static str { "==" }
473    fn identity(&self) -> &'static str { " == " } // Rust equality
474    fn wrap_identity(&self) -> bool { true } // Wrap in parens for valid Rust
475
476    // Use full variable names, not abbreviations
477    fn use_full_names(&self) -> bool { true }
478    fn preserve_case(&self) -> bool { true } // Keep original variable case
479
480    // Quantifiers: runtime can't check universal quantification, emit comments
481    fn universal(&self) -> String { "/* ∀ */".to_string() }
482    fn existential(&self) -> String { "/* ∃ */".to_string() }
483    fn cardinal(&self, n: u32) -> String { format!("/* ∃={} */", n) }
484    fn at_least(&self, n: u32) -> String { format!("/* ∃≥{} */", n) }
485    fn at_most(&self, n: u32) -> String { format!("/* ∃≤{} */", n) }
486
487    // Modal/Temporal operators are stripped for runtime (not checkable)
488    fn necessity(&self) -> &'static str { "" }
489    fn possibility(&self) -> &'static str { "" }
490    fn past(&self) -> &'static str { "" }
491    fn future(&self) -> &'static str { "" }
492    fn progressive(&self) -> &'static str { "" }
493    fn perfect(&self) -> &'static str { "" }
494    fn habitual(&self) -> &'static str { "" }
495    fn iterative(&self) -> &'static str { "" }
496    fn passive(&self) -> &'static str { "" }
497    fn categorical_all(&self) -> &'static str { "" }
498    fn categorical_no(&self) -> &'static str { "" }
499    fn categorical_some(&self) -> &'static str { "" }
500    fn categorical_not(&self) -> &'static str { "" }
501
502    fn lambda(&self, var: &str, body: &str) -> String {
503        format!("|{}| {{ {} }}", var, body)
504    }
505
506    fn counterfactual(&self, a: &str, c: &str) -> String {
507        format!("/* if {} then {} */", a, c)
508    }
509
510    fn superlative(&self, _: &str, _: &str, _: &str) -> String {
511        "/* superlative */".to_string()
512    }
513
514    // Override comparative for Rust: map adjectives to comparison operators
515    fn write_comparative<W: Write>(
516        &self,
517        w: &mut W,
518        adjective: &str,
519        subject: &str,
520        object: &str,
521        _difference: Option<&str>,
522    ) -> std::fmt::Result {
523        let adj_lower = adjective.to_lowercase();
524        match adj_lower.as_str() {
525            "great" | "big" | "large" | "tall" | "old" | "high" | "more" | "greater" => {
526                write!(w, "({} > {})", subject, object)
527            }
528            "small" | "little" | "short" | "young" | "low" | "less" | "fewer" => {
529                write!(w, "({} < {})", subject, object)
530            }
531            _ => write!(w, "({} > {})", subject, object) // default to greater-than
532        }
533    }
534
535    // Override unary_op to wrap in parens for valid Rust
536    fn unary_op(&self, op: &TokenType, operand: &str) -> String {
537        match op {
538            TokenType::Not => format!("(!{})", operand),
539            _ => format!("/* unknown unary */({})", operand),
540        }
541    }
542
543    // Override binary_op for implication desugaring: A → B = !A || B
544    fn binary_op(&self, op: &TokenType, left: &str, right: &str) -> String {
545        match op {
546            TokenType::If | TokenType::Then => format!("(!({}) || ({}))", left, right),
547            TokenType::And => format!("({} && {})", left, right),
548            TokenType::Or => format!("({} || {})", left, right),
549            TokenType::Iff => format!("({} == {})", left, right),
550            _ => "/* unknown op */".to_string(),
551        }
552    }
553
554    // Core predicate mapping: semantic interpretation of predicates to Rust operators
555    fn write_predicate<W: Write>(
556        &self,
557        w: &mut W,
558        name: &str,
559        args: &[Term],
560        _registry: &mut SymbolRegistry,
561        interner: &Interner,
562    ) -> std::fmt::Result {
563        // Helper to render a term at given index to a string, preserving original case
564        let render = |idx: usize| -> String {
565            let mut buf = String::new();
566            if let Some(arg) = args.get(idx) {
567                let _ = arg.write_to_raw(&mut buf, interner);
568            }
569            buf
570        };
571
572        match name.to_lowercase().as_str() {
573            // Comparisons
574            "greater" if args.len() == 2 => write!(w, "({} > {})", render(0), render(1)),
575            "less" if args.len() == 2 => write!(w, "({} < {})", render(0), render(1)),
576            "equal" | "equals" if args.len() == 2 => write!(w, "({} == {})", render(0), render(1)),
577            "notequal" | "not_equal" if args.len() == 2 => write!(w, "({} != {})", render(0), render(1)),
578            "greaterequal" | "atleast" | "at_least" if args.len() == 2 => write!(w, "({} >= {})", render(0), render(1)),
579            "lessequal" | "atmost" | "at_most" if args.len() == 2 => write!(w, "({} <= {})", render(0), render(1)),
580
581            // Unary checks
582            "positive" if args.len() == 1 => write!(w, "({} > 0)", render(0)),
583            "negative" if args.len() == 1 => write!(w, "({} < 0)", render(0)),
584            "zero" if args.len() == 1 => write!(w, "({} == 0)", render(0)),
585            "empty" if args.len() == 1 => write!(w, "{}.is_empty()", render(0)),
586
587            // Collection membership
588            "in" if args.len() == 2 => write!(w, "{}.contains(&{})", render(1), render(0)),
589            "contains" if args.len() == 2 => write!(w, "{}.contains(&{})", render(0), render(1)),
590
591            // Fallback: method call for 1 arg, function call for N args
592            _ if args.len() == 1 => write!(w, "{}.is_{}()", render(0), name.to_lowercase()),
593            _ => {
594                write!(w, "{}(", name.to_lowercase())?;
595                for i in 0..args.len() {
596                    if i > 0 { write!(w, ", ")?; }
597                    write!(w, "{}", render(i))?;
598                }
599                write!(w, ")")
600            }
601        }
602    }
603}
604
605#[cfg(test)]
606mod tests {
607    use super::*;
608
609    #[test]
610    fn unicode_binary_operators() {
611        let f = UnicodeFormatter;
612        assert_eq!(f.binary_op(&TokenType::And, "P", "Q"), "(P ∧ Q)");
613        assert_eq!(f.binary_op(&TokenType::Or, "P", "Q"), "(P ∨ Q)");
614        assert_eq!(f.binary_op(&TokenType::If, "P", "Q"), "(P → Q)");
615        assert_eq!(f.binary_op(&TokenType::Iff, "P", "Q"), "(P ↔ Q)");
616    }
617
618    #[test]
619    fn latex_binary_operators() {
620        let f = LatexFormatter;
621        assert_eq!(f.binary_op(&TokenType::And, "P", "Q"), r"(P \cdot Q)");
622        assert_eq!(f.binary_op(&TokenType::Or, "P", "Q"), r"(P \vee Q)");
623        assert_eq!(f.binary_op(&TokenType::If, "P", "Q"), r"(P \supset Q)");
624        assert_eq!(f.binary_op(&TokenType::Iff, "P", "Q"), r"(P \equiv Q)");
625    }
626
627    #[test]
628    fn unicode_quantifiers() {
629        let f = UnicodeFormatter;
630        assert_eq!(f.quantifier(&QuantifierKind::Universal, "x", "P(x)"), "∀x(P(x))");
631        assert_eq!(f.quantifier(&QuantifierKind::Existential, "x", "P(x)"), "∃x(P(x))");
632        assert_eq!(f.quantifier(&QuantifierKind::Cardinal(3), "x", "P(x)"), "∃=3.x(P(x))");
633    }
634
635    #[test]
636    fn latex_quantifiers() {
637        let f = LatexFormatter;
638        assert_eq!(f.quantifier(&QuantifierKind::Universal, "x", "P(x)"), "\\forall x(P(x))");
639        assert_eq!(f.quantifier(&QuantifierKind::Existential, "x", "P(x)"), "\\exists x(P(x))");
640    }
641
642    #[test]
643    fn latex_sanitization() {
644        let f = LatexFormatter;
645        assert_eq!(f.sanitize("foo_bar"), r"foo\_bar");
646        assert_eq!(f.sanitize("x^2"), r"x\^{}2");
647        assert_eq!(f.sanitize("a&b"), r"a\&b");
648    }
649
650    #[test]
651    fn unicode_no_sanitization() {
652        let f = UnicodeFormatter;
653        assert_eq!(f.sanitize("foo_bar"), "foo_bar");
654    }
655
656    #[test]
657    fn unicode_lambda() {
658        let f = UnicodeFormatter;
659        assert_eq!(f.lambda("x", "P(x)"), "λx.P(x)");
660    }
661
662    #[test]
663    fn latex_lambda() {
664        let f = LatexFormatter;
665        assert_eq!(f.lambda("x", "P(x)"), "\\lambda x.P(x)");
666    }
667
668    #[test]
669    fn unicode_counterfactual() {
670        let f = UnicodeFormatter;
671        assert_eq!(f.counterfactual("P", "Q"), "(P □→ Q)");
672    }
673
674    #[test]
675    fn latex_counterfactual() {
676        let f = LatexFormatter;
677        assert_eq!(f.counterfactual("P", "Q"), r"(P \boxright Q)");
678    }
679
680    // RustFormatter tests
681    #[test]
682    fn rust_binary_operators() {
683        let f = RustFormatter;
684        assert_eq!(f.binary_op(&TokenType::And, "P", "Q"), "(P && Q)");
685        assert_eq!(f.binary_op(&TokenType::Or, "P", "Q"), "(P || Q)");
686        assert_eq!(f.binary_op(&TokenType::Iff, "P", "Q"), "(P == Q)");
687    }
688
689    #[test]
690    fn rust_implication_desugaring() {
691        let f = RustFormatter;
692        // A → B desugars to !A || B
693        assert_eq!(f.binary_op(&TokenType::If, "P", "Q"), "(!(P) || (Q))");
694    }
695
696    #[test]
697    fn rust_lambda() {
698        let f = RustFormatter;
699        assert_eq!(f.lambda("x", "x > 0"), "|x| { x > 0 }");
700    }
701
702    #[test]
703    fn rust_quantifiers_as_comments() {
704        let f = RustFormatter;
705        assert_eq!(f.universal(), "/* ∀ */");
706        assert_eq!(f.existential(), "/* ∃ */");
707    }
708}