Skip to main content

logicaffeine_language/
transpile.rs

1//! Transpilation from AST to first-order logic notation.
2//!
3//! This module converts the internal AST representation to human-readable
4//! logical formulas in various output formats.
5//!
6//! ## Output Formats
7//!
8//! | Format | Example | Use Case |
9//! |--------|---------|----------|
10//! | **Unicode** | ∀x(Cat(x) → Sleeps(x)) | Terminal display |
11//! | **LaTeX** | `\forall x(Cat(x) \to Sleeps(x))` | Academic papers |
12//! | **ASCII** | `Ax(Cat(x) -> Sleeps(x))` | Plain text |
13//! | **Kripke** | `□(P) @w0` | Modal logic diagrams |
14//!
15//! ## Neo-Davidsonian Events
16//!
17//! Verb semantics use event variables following Davidson's event semantics:
18//!
19//! ```text
20//! "John loves Mary"
21//! → ∃e(Love(e) ∧ Agent(e, john) ∧ Theme(e, mary))
22//! ```
23//!
24//! This representation enables reasoning about event modification,
25//! aspect, and temporal relations.
26
27use std::fmt::Write;
28
29use crate::ast::{LogicExpr, NounPhrase, Term, QuantifierKind};
30use crate::ast::logic::NumberKind;
31use crate::formatter::{KripkeFormatter, LatexFormatter, LogicFormatter, SimpleFOLFormatter, UnicodeFormatter};
32use logicaffeine_base::{Interner, Symbol};
33use crate::registry::SymbolRegistry;
34use crate::token::TokenType;
35use crate::{OutputFormat, TranspileContext};
36
37/// Collect event variables from NeoEvents with suppress_existential=true
38/// Returns unique event variables (coordinated weather verbs share the same var)
39fn collect_suppress_existential_events<'a>(expr: &LogicExpr<'a>) -> Vec<Symbol> {
40    let mut events = Vec::new();
41    collect_suppress_existential_events_inner(expr, &mut events);
42    // Deduplicate - coordinated weather verbs share the same event variable
43    // Symbol is Copy so we can use a simple O(n^2) dedup
44    let mut unique = Vec::new();
45    for e in events {
46        if !unique.iter().any(|x| *x == e) {
47            unique.push(e);
48        }
49    }
50    unique
51}
52
53fn collect_suppress_existential_events_inner<'a>(expr: &LogicExpr<'a>, events: &mut Vec<Symbol>) {
54    match expr {
55        LogicExpr::NeoEvent(data) => {
56            if data.suppress_existential {
57                events.push(data.event_var);
58            }
59        }
60        LogicExpr::BinaryOp { left, right, .. } => {
61            collect_suppress_existential_events_inner(left, events);
62            collect_suppress_existential_events_inner(right, events);
63        }
64        LogicExpr::UnaryOp { operand, .. } => {
65            collect_suppress_existential_events_inner(operand, events);
66        }
67        LogicExpr::Temporal { body, .. } => {
68            collect_suppress_existential_events_inner(body, events);
69        }
70        LogicExpr::TemporalBinary { left, right, .. } => {
71            collect_suppress_existential_events_inner(left, events);
72            collect_suppress_existential_events_inner(right, events);
73        }
74        LogicExpr::Aspectual { body, .. } => {
75            collect_suppress_existential_events_inner(body, events);
76        }
77        LogicExpr::Modal { operand, .. } => {
78            collect_suppress_existential_events_inner(operand, events);
79        }
80        _ => {}
81    }
82}
83
84/// Capitalizes the first character of a string.
85///
86/// Used for predicate names in logic output (e.g., "cat" → "Cat").
87pub fn capitalize_first(s: &str) -> String {
88    let mut chars = s.chars();
89    match chars.next() {
90        None => String::new(),
91        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
92    }
93}
94
95fn write_capitalized<W: Write>(w: &mut W, s: &str) -> std::fmt::Result {
96    let mut chars = s.chars();
97    match chars.next() {
98        None => Ok(()),
99        Some(c) => {
100            for uc in c.to_uppercase() {
101                write!(w, "{}", uc)?;
102            }
103            write!(w, "{}", chars.as_str())
104        }
105    }
106}
107
108impl<'a> NounPhrase<'a> {
109    /// Converts the noun phrase to a short logic symbol (e.g., "C" for "cat").
110    pub fn to_symbol(&self, registry: &mut SymbolRegistry, interner: &Interner) -> String {
111        registry.get_symbol(self.noun, interner)
112    }
113
114    /// Converts the noun phrase to a full logic symbol (e.g., "Cat" for "cat").
115    pub fn to_symbol_full(&self, registry: &SymbolRegistry, interner: &Interner) -> String {
116        registry.get_symbol_full(self.noun, interner)
117    }
118}
119
120impl<'a> Term<'a> {
121    /// Writes the term to a writer using abbreviated symbols.
122    pub fn write_to<W: Write>(
123        &self,
124        w: &mut W,
125        registry: &mut SymbolRegistry,
126        interner: &Interner,
127    ) -> std::fmt::Result {
128        self.write_to_inner(w, registry, interner, false)
129    }
130
131    /// Writes the term to a writer using full (unabbreviated) symbols.
132    pub fn write_to_full<W: Write>(
133        &self,
134        w: &mut W,
135        registry: &mut SymbolRegistry,
136        interner: &Interner,
137    ) -> std::fmt::Result {
138        self.write_to_inner(w, registry, interner, true)
139    }
140
141    /// Writes the term preserving original case (for code generation).
142    pub fn write_to_raw<W: Write>(
143        &self,
144        w: &mut W,
145        interner: &Interner,
146    ) -> std::fmt::Result {
147        match self {
148            Term::Constant(name) | Term::Variable(name) => {
149                write!(w, "{}", interner.resolve(*name))
150            }
151            Term::Function(name, args) => {
152                write!(w, "{}(", interner.resolve(*name))?;
153                for (i, arg) in args.iter().enumerate() {
154                    if i > 0 {
155                        write!(w, ", ")?;
156                    }
157                    arg.write_to_raw(w, interner)?;
158                }
159                write!(w, ")")
160            }
161            Term::Group(members) => {
162                write!(w, "(")?;
163                for (i, m) in members.iter().enumerate() {
164                    if i > 0 {
165                        write!(w, ", ")?;
166                    }
167                    m.write_to_raw(w, interner)?;
168                }
169                write!(w, ")")
170            }
171            Term::Possessed { possessor, possessed } => {
172                possessor.write_to_raw(w, interner)?;
173                write!(w, ".{}", interner.resolve(*possessed))
174            }
175            Term::Value { kind, .. } => match kind {
176                NumberKind::Integer(n) => write!(w, "{}", n),
177                NumberKind::Real(f) => write!(w, "{}", f),
178                NumberKind::Symbolic(s) => write!(w, "{}", interner.resolve(*s)),
179            }
180            Term::Sigma(predicate) => write!(w, "σ({})", interner.resolve(*predicate)),
181            Term::Intension(predicate) => write!(w, "^{}", interner.resolve(*predicate)),
182            Term::Kind(kind) => write!(w, "^{}", interner.resolve(*kind)),
183            Term::Proposition(expr) => write!(w, "[proposition]"),
184        }
185    }
186
187    fn write_to_inner<W: Write>(
188        &self,
189        w: &mut W,
190        registry: &mut SymbolRegistry,
191        interner: &Interner,
192        use_full_names: bool,
193    ) -> std::fmt::Result {
194        match self {
195            Term::Constant(name) => {
196                if use_full_names {
197                    write!(w, "{}", registry.get_symbol_full(*name, interner))
198                } else {
199                    write!(w, "{}", registry.get_symbol(*name, interner))
200                }
201            }
202            Term::Variable(name) => write!(w, "{}", interner.resolve(*name)),
203            Term::Function(name, args) => {
204                let fn_name = if use_full_names {
205                    registry.get_symbol_full(*name, interner)
206                } else {
207                    registry.get_symbol(*name, interner)
208                };
209                write!(w, "{}(", fn_name)?;
210                for (i, arg) in args.iter().enumerate() {
211                    if i > 0 {
212                        write!(w, ", ")?;
213                    }
214                    arg.write_to_inner(w, registry, interner, use_full_names)?;
215                }
216                write!(w, ")")
217            }
218            Term::Group(members) => {
219                for (i, m) in members.iter().enumerate() {
220                    if i > 0 {
221                        write!(w, " ⊕ ")?;
222                    }
223                    m.write_to_inner(w, registry, interner, use_full_names)?;
224                }
225                Ok(())
226            }
227            Term::Possessed { possessor, possessed } => {
228                let poss_name = if use_full_names {
229                    registry.get_symbol_full(*possessed, interner)
230                } else {
231                    registry.get_symbol(*possessed, interner)
232                };
233                write!(w, "Poss(")?;
234                possessor.write_to_inner(w, registry, interner, use_full_names)?;
235                write!(w, ", {})", poss_name)
236            }
237            Term::Sigma(predicate) => {
238                let pred_name = if use_full_names {
239                    registry.get_symbol_full(*predicate, interner)
240                } else {
241                    registry.get_symbol(*predicate, interner)
242                };
243                write!(w, "σ{}", pred_name)
244            }
245            Term::Intension(predicate) => {
246                // Use full word for intensional terms, not abbreviated symbol
247                let word = interner.resolve(*predicate);
248                let capitalized = word.chars().next()
249                    .map(|c| c.to_uppercase().collect::<String>() + &word[1..])
250                    .unwrap_or_default();
251                write!(w, "^{}", capitalized)
252            }
253            Term::Kind(kind) => {
254                // Kind terms render with the full word, like intensions: ^Tooth.
255                let word = interner.resolve(*kind);
256                let capitalized = word.chars().next()
257                    .map(|c| c.to_uppercase().collect::<String>() + &word[1..])
258                    .unwrap_or_default();
259                write!(w, "^{}", capitalized)
260            }
261            Term::Proposition(expr) => {
262                write!(w, "[")?;
263                expr.write_logic(w, registry, interner, &UnicodeFormatter)?;
264                write!(w, "]")
265            }
266            Term::Value { kind, unit, dimension: _ } => {
267                use crate::ast::NumberKind;
268                match kind {
269                    NumberKind::Real(r) => write!(w, "{}", r)?,
270                    NumberKind::Integer(i) => write!(w, "{}", i)?,
271                    NumberKind::Symbolic(s) => write!(w, "{}", interner.resolve(*s))?,
272                }
273                if let Some(u) = unit {
274                    write!(w, " {}", interner.resolve(*u))?;
275                }
276                Ok(())
277            }
278        }
279    }
280
281    /// Transpiles the term to a logic formula string.
282    pub fn transpile(&self, registry: &mut SymbolRegistry, interner: &Interner) -> String {
283        let mut buf = String::new();
284        let _ = self.write_to(&mut buf, registry, interner);
285        buf
286    }
287}
288
289/// Extracts top-level conjuncts from a discourse (sentences combined with AND).
290/// Returns a vector of individual sentence expressions.
291fn collect_discourse_conjuncts<'a>(expr: &'a LogicExpr<'a>) -> Vec<&'a LogicExpr<'a>> {
292    let mut conjuncts = Vec::new();
293    collect_discourse_conjuncts_inner(expr, &mut conjuncts);
294    conjuncts
295}
296
297fn collect_discourse_conjuncts_inner<'a>(expr: &'a LogicExpr<'a>, conjuncts: &mut Vec<&'a LogicExpr<'a>>) {
298    match expr {
299        LogicExpr::BinaryOp { left, op: TokenType::And, right } => {
300            // Recursively collect from both sides
301            collect_discourse_conjuncts_inner(left, conjuncts);
302            collect_discourse_conjuncts_inner(right, conjuncts);
303        }
304        _ => {
305            // This is a leaf sentence (not a top-level conjunction)
306            conjuncts.push(expr);
307        }
308    }
309}
310
311impl<'a> LogicExpr<'a> {
312    /// Transpile a discourse (multiple sentences) as numbered formulas.
313    /// If the expression is a top-level conjunction of sentences, formats as:
314    /// ```text
315    /// 1) formula1
316    /// 2) formula2
317    /// 3) formula3
318    /// ```
319    /// If it's a single sentence, just returns the formula without numbering.
320    pub fn transpile_discourse(
321        &self,
322        registry: &mut SymbolRegistry,
323        interner: &Interner,
324        format: OutputFormat,
325    ) -> String {
326        let conjuncts = collect_discourse_conjuncts(self);
327
328        if conjuncts.len() <= 1 {
329            // Single sentence - no numbering needed
330            return self.transpile(registry, interner, format);
331        }
332
333        // Multiple sentences - format as numbered list
334        let mut result = String::new();
335        for (i, conjunct) in conjuncts.iter().enumerate() {
336            if i > 0 {
337                result.push('\n');
338            }
339            let formula = conjunct.transpile(registry, interner, format);
340            result.push_str(&format!("{}) {}", i + 1, formula));
341        }
342        result
343    }
344
345    pub fn write_logic<W: Write, F: LogicFormatter>(
346        &self,
347        w: &mut W,
348        registry: &mut SymbolRegistry,
349        interner: &Interner,
350        fmt: &F,
351    ) -> std::fmt::Result {
352        match self {
353            LogicExpr::Predicate { name, args, world } => {
354                let pred_name = if fmt.use_full_names() {
355                    registry.get_symbol_full(*name, interner)
356                } else {
357                    registry.get_symbol(*name, interner)
358                };
359
360                // If formatter wants world arguments and we have one, append it
361                if fmt.include_world_arguments() {
362                    if let Some(w_sym) = world {
363                        // Build extended args with world variable appended
364                        let mut extended: Vec<Term> = args.to_vec();
365                        extended.push(Term::Variable(*w_sym));
366                        return fmt.write_predicate(w, &pred_name, &extended, registry, interner);
367                    }
368                }
369                fmt.write_predicate(w, &pred_name, args, registry, interner)
370            }
371
372            LogicExpr::Identity { left, right } => {
373                if fmt.wrap_identity() {
374                    write!(w, "(")?;
375                }
376                if fmt.preserve_case() {
377                    left.write_to_raw(w, interner)?;
378                } else if fmt.use_full_names() {
379                    left.write_to_full(w, registry, interner)?;
380                } else {
381                    left.write_to(w, registry, interner)?;
382                }
383                write!(w, "{}", fmt.identity())?;
384                if fmt.preserve_case() {
385                    right.write_to_raw(w, interner)?;
386                } else if fmt.use_full_names() {
387                    right.write_to_full(w, registry, interner)?;
388                } else {
389                    right.write_to(w, registry, interner)?;
390                }
391                if fmt.wrap_identity() {
392                    write!(w, ")")?;
393                }
394                Ok(())
395            }
396
397            LogicExpr::Metaphor { tenor, vehicle } => {
398                write!(w, "Metaphor(")?;
399                tenor.write_to(w, registry, interner)?;
400                write!(w, ", ")?;
401                vehicle.write_to(w, registry, interner)?;
402                write!(w, ")")
403            }
404
405            LogicExpr::Quantifier { kind, variable, body, .. } => {
406                let var_str = interner.resolve(*variable);
407
408                // In SimpleFOL mode, skip event quantifiers (variables named "e" or starting with "e" followed by digits)
409                if fmt.use_simple_events() && (var_str == "e" || var_str.starts_with("e") && var_str[1..].chars().all(|c| c.is_ascii_digit())) {
410                    return body.write_logic(w, registry, interner, fmt);
411                }
412
413                let mut body_buf = String::new();
414                body.write_logic(&mut body_buf, registry, interner, fmt)?;
415                write!(w, "{}", fmt.quantifier(kind, var_str, &body_buf))
416            }
417
418            LogicExpr::Categorical(data) => {
419                let s = if fmt.use_full_names() {
420                    fmt.sanitize(&data.subject.to_symbol_full(registry, interner))
421                } else {
422                    fmt.sanitize(&data.subject.to_symbol(registry, interner))
423                };
424                let p = if fmt.use_full_names() {
425                    fmt.sanitize(&data.predicate.to_symbol_full(registry, interner))
426                } else {
427                    fmt.sanitize(&data.predicate.to_symbol(registry, interner))
428                };
429                match (&data.quantifier, data.copula_negative) {
430                    (TokenType::All, false) => write!(w, "{} {} is {}", fmt.categorical_all(), s, p),
431                    (TokenType::No, false) => write!(w, "{} {} is {}", fmt.categorical_no(), s, p),
432                    (TokenType::Some, false) => write!(w, "{} {} is {}", fmt.categorical_some(), s, p),
433                    (TokenType::Some, true) => write!(w, "{} {} is {} {}", fmt.categorical_some(), s, fmt.categorical_not(), p),
434                    (TokenType::All, true) => write!(w, "{} {} is {} {}", fmt.categorical_some(), s, fmt.categorical_not(), p),
435                    _ => write!(w, "Invalid Syllogism"),
436                }
437            }
438
439            LogicExpr::Relation(data) => {
440                let s = if fmt.use_full_names() {
441                    data.subject.to_symbol_full(registry, interner)
442                } else {
443                    data.subject.to_symbol(registry, interner)
444                };
445                let v = if fmt.use_full_names() {
446                    fmt.sanitize(&registry.get_symbol_full(data.verb, interner))
447                } else {
448                    fmt.sanitize(&registry.get_symbol(data.verb, interner))
449                };
450                let o = if fmt.use_full_names() {
451                    data.object.to_symbol_full(registry, interner)
452                } else {
453                    data.object.to_symbol(registry, interner)
454                };
455                write!(w, "{}({}, {})", v, s, o)
456            }
457
458            LogicExpr::Modal { vector, operand } => {
459                let mut o = String::new();
460                operand.write_logic(&mut o, registry, interner, fmt)?;
461                // An evidential renders by its evidence source (the Kratzer
462                // modal base): Seem(⟨Happy(John)⟩) — never as a bare □/◇,
463                // since the complement is unasserted in every notation.
464                if vector.flavor == crate::ast::ModalFlavor::Evidential {
465                    let source = match vector.modal_base {
466                        Some(base) if fmt.use_full_names() => {
467                            registry.get_symbol_full(base, interner)
468                        }
469                        Some(base) => registry.get_symbol(base, interner),
470                        None => "Seem".to_string(),
471                    };
472                    return write!(w, "{}([{}])", source, o);
473                }
474                write!(w, "{}", fmt.modal(vector.domain, vector.force, &o))
475            }
476
477            LogicExpr::BinaryOp { left, op, right } => {
478                let mut l = String::new();
479                let mut r = String::new();
480                left.write_logic(&mut l, registry, interner, fmt)?;
481                right.write_logic(&mut r, registry, interner, fmt)?;
482
483                // For conditionals (If), check if there are suppress_existential events
484                // that need universal quantification (DRS semantics for generic conditionals)
485                if matches!(op, TokenType::If | TokenType::Implies) {
486                    let events = collect_suppress_existential_events(self);
487                    if !events.is_empty() {
488                        // Wrap with universal quantifiers for each event variable
489                        let mut result = fmt.binary_op(op, &l, &r);
490                        for event_var in events.into_iter().rev() {
491                            let var_str = interner.resolve(event_var);
492                            result = fmt.quantifier(&QuantifierKind::Universal, var_str, &result);
493                        }
494                        return write!(w, "{}", result);
495                    }
496                }
497
498                write!(w, "{}", fmt.binary_op(op, &l, &r))
499            }
500
501            LogicExpr::UnaryOp { op, operand } => {
502                let mut o = String::new();
503                operand.write_logic(&mut o, registry, interner, fmt)?;
504                write!(w, "{}", fmt.unary_op(op, &o))
505            }
506
507            LogicExpr::Temporal { operator, body } => {
508                let mut inner = String::new();
509                body.write_logic(&mut inner, registry, interner, fmt)?;
510                write!(w, "{}", fmt.temporal(operator, &inner))
511            }
512
513            LogicExpr::TemporalBinary { operator, left, right } => {
514                let mut l = String::new();
515                let mut r = String::new();
516                left.write_logic(&mut l, registry, interner, fmt)?;
517                right.write_logic(&mut r, registry, interner, fmt)?;
518                write!(w, "{}", fmt.temporal_binary(operator, &l, &r))
519            }
520
521            LogicExpr::Aspectual { operator, body } => {
522                let mut inner = String::new();
523                body.write_logic(&mut inner, registry, interner, fmt)?;
524                write!(w, "{}", fmt.aspectual(operator, &inner))
525            }
526
527            LogicExpr::Voice { operator, body } => {
528                let mut inner = String::new();
529                body.write_logic(&mut inner, registry, interner, fmt)?;
530                write!(w, "{}", fmt.voice(operator, &inner))
531            }
532
533            LogicExpr::Question { wh_variable, body } => {
534                let mut body_str = String::new();
535                body.write_logic(&mut body_str, registry, interner, fmt)?;
536                write!(w, "{}", fmt.lambda(interner.resolve(*wh_variable), &body_str))
537            }
538
539            LogicExpr::YesNoQuestion { body } => {
540                write!(w, "?")?;
541                body.write_logic(w, registry, interner, fmt)
542            }
543
544            LogicExpr::Atom(s) => {
545                let name = if fmt.preserve_case() {
546                    interner.resolve(*s).to_string()
547                } else if fmt.use_full_names() {
548                    registry.get_symbol_full(*s, interner)
549                } else {
550                    registry.get_symbol(*s, interner)
551                };
552                write!(w, "{}", fmt.sanitize(&name))
553            }
554
555            LogicExpr::Lambda { variable, body } => {
556                let mut b = String::new();
557                body.write_logic(&mut b, registry, interner, fmt)?;
558                write!(w, "{}", fmt.lambda(interner.resolve(*variable), &b))
559            }
560
561            LogicExpr::App { function, argument } => {
562                write!(w, "(")?;
563                function.write_logic(w, registry, interner, fmt)?;
564                write!(w, ")(")?;
565                argument.write_logic(w, registry, interner, fmt)?;
566                write!(w, ")")
567            }
568
569            LogicExpr::Intensional { operator, content } => {
570                write!(w, "{}[", fmt.sanitize(&registry.get_symbol(*operator, interner)))?;
571                content.write_logic(w, registry, interner, fmt)?;
572                write!(w, "]")
573            }
574
575            LogicExpr::Event { predicate, adverbs } => {
576                let mut pred_str = String::new();
577                predicate.write_logic(&mut pred_str, registry, interner, fmt)?;
578                let adverb_preds: Vec<String> = adverbs
579                    .iter()
580                    .map(|a| format!("{}(e)", fmt.sanitize(&registry.get_symbol(*a, interner))))
581                    .collect();
582                write!(w, "{}", fmt.event_quantifier(&pred_str, &adverb_preds))
583            }
584
585            LogicExpr::NeoEvent(data) => {
586                use crate::ast::{QuantifierKind, ThematicRole};
587
588                if fmt.use_simple_events() {
589                    write!(w, "{}", registry.get_symbol_full(data.verb, interner))?;
590                    write!(w, "(")?;
591                    let mut first = true;
592                    let mut subject_term = None;
593                    for (role, term) in data.roles.iter() {
594                        // Include core thematic roles in SimpleFOL output
595                        if matches!(role, ThematicRole::Agent | ThematicRole::Patient | ThematicRole::Theme | ThematicRole::Goal | ThematicRole::Location) {
596                            if !first {
597                                write!(w, ", ")?;
598                            }
599                            first = false;
600                            term.write_to_full(w, registry, interner)?;
601                            // The adverb reattaches to the agent (or the first core
602                            // participant when there is no agent).
603                            if subject_term.is_none() || matches!(role, ThematicRole::Agent) {
604                                subject_term = Some(term);
605                            }
606                        }
607                    }
608                    write!(w, ")")?;
609                    // Flattening the event drops the ∃e binder, so an adverbial
610                    // modifier `Loudly(e)` must reattach to the participant, or it is
611                    // silently lost: ∃e(Bark(e) ∧ Agent(e,y) ∧ Loudly(e)) ⇒ Bark(y) ∧ Loudly(y).
612                    for mod_sym in data.modifiers.iter() {
613                        write!(w, " {} ", fmt.and())?;
614                        write_capitalized(w, interner.resolve(*mod_sym))?;
615                        write!(w, "(")?;
616                        if let Some(term) = subject_term {
617                            term.write_to_full(w, registry, interner)?;
618                        }
619                        write!(w, ")")?;
620                    }
621                    Ok(())
622                } else {
623                    let e = interner.resolve(data.event_var);
624                    let mut body = String::new();
625
626                    // Get world argument suffix if Kripke format
627                    let world_suffix = if fmt.include_world_arguments() {
628                        data.world.map(|w| format!(", {}", interner.resolve(w))).unwrap_or_default()
629                    } else {
630                        String::new()
631                    };
632
633                    write_capitalized(&mut body, interner.resolve(data.verb))?;
634                    write!(body, "({}{})", e, world_suffix)?;
635                    for (role, term) in data.roles.iter() {
636                        let role_str = match role {
637                            ThematicRole::Agent => "Agent",
638                            ThematicRole::Patient => "Patient",
639                            ThematicRole::Theme => "Theme",
640                            ThematicRole::Recipient => "Recipient",
641                            ThematicRole::Goal => "Goal",
642                            ThematicRole::Source => "Source",
643                            ThematicRole::Instrument => "Instrument",
644                            ThematicRole::Location => "Location",
645                            ThematicRole::Time => "Time",
646                            ThematicRole::Manner => "Manner",
647                            ThematicRole::Result => "Result",
648                            ThematicRole::Depictive => "Depictive",
649                        };
650                        write!(body, " {} {}({}, ", fmt.and(), role_str, e)?;
651                        if fmt.use_full_names() {
652                            term.write_to_full(&mut body, registry, interner)?;
653                        } else {
654                            term.write_to(&mut body, registry, interner)?;
655                        }
656                        write!(body, "{})", world_suffix)?;
657                    }
658                    for mod_sym in data.modifiers.iter() {
659                        write!(body, " {} ", fmt.and())?;
660                        write_capitalized(&mut body, interner.resolve(*mod_sym))?;
661                        write!(body, "({}{})", e, world_suffix)?;
662                    }
663                    if data.suppress_existential {
664                        // Event var will be bound by outer ∀ from DRS (generic conditionals)
665                        write!(w, "{}", body)
666                    } else {
667                        // Normal case: emit ∃e(...)
668                        write!(w, "{}", fmt.quantifier(&QuantifierKind::Existential, e, &body))
669                    }
670                }
671            }
672
673            LogicExpr::Imperative { action } => {
674                // Directive(hearer, ⟨action⟩) — the commanded action is the
675                // content of an obligation on the addressee (§1.4), not an
676                // asserted event. The addressee is the action's Agent role
677                // (Addressee, or Us for hortative "let's").
678                use crate::ast::ThematicRole;
679                fn directive_agent<'b>(e: &'b LogicExpr<'b>) -> Option<&'b Term<'b>> {
680                    match e {
681                        LogicExpr::NeoEvent(data) => data
682                            .roles
683                            .iter()
684                            .find(|(role, _)| matches!(role, ThematicRole::Agent))
685                            .map(|(_, term)| term),
686                        LogicExpr::UnaryOp { operand, .. } => directive_agent(operand),
687                        LogicExpr::Quantifier { body, .. } => directive_agent(body),
688                        LogicExpr::BinaryOp { left, .. } => directive_agent(left),
689                        _ => None,
690                    }
691                }
692                let mut addressee = String::new();
693                match directive_agent(action) {
694                    Some(t) if fmt.use_full_names() => {
695                        t.write_to_full(&mut addressee, registry, interner)?
696                    }
697                    Some(t) => t.write_to(&mut addressee, registry, interner)?,
698                    None => addressee.push_str("Addressee"),
699                }
700                let mut a = String::new();
701                action.write_logic(&mut a, registry, interner, fmt)?;
702                write!(w, "Directive({}, [{}])", addressee, a)
703            }
704
705            LogicExpr::Exclamative { degree_var, body } => {
706                // Exclaim(∃d(body ∧ d ≫ θ)): a surprisingly-high degree.
707                let d = interner.resolve(*degree_var);
708                write!(w, "Exclaim(∃{}(", d)?;
709                body.write_logic(w, registry, interner, fmt)?;
710                write!(w, " {} {} ≫ θ))", fmt.and(), d)
711            }
712
713            LogicExpr::Optative { wish } => {
714                // Wish(Speaker, ⟨wish⟩): the complement is not asserted.
715                write!(w, "Wish(Speaker, ⟨")?;
716                wish.write_logic(w, registry, interner, fmt)?;
717                write!(w, "⟩)")
718            }
719
720            LogicExpr::Implicature { assertion, implicature } => {
721                // assertion +> Implicature(…): literal meaning, then the cancellable
722                // scalar implicature (the defeasible part).
723                assertion.write_logic(w, registry, interner, fmt)?;
724                write!(w, " +> Implicature(")?;
725                implicature.write_logic(w, registry, interner, fmt)?;
726                write!(w, ")")
727            }
728
729            LogicExpr::SpeechAct { performer, act_type, content } => {
730                // Render the performer with its full name (e.g. Speaker), matching the
731                // role constants used inside the content, rather than an abbreviation.
732                write!(w, "SpeechAct({}, {}, ", interner.resolve(*act_type), fmt.sanitize(&registry.get_symbol_full(*performer, interner)))?;
733                content.write_logic(w, registry, interner, fmt)?;
734                write!(w, ")")
735            }
736
737            LogicExpr::Counterfactual { antecedent, consequent } => {
738                let mut a = String::new();
739                let mut c = String::new();
740                antecedent.write_logic(&mut a, registry, interner, fmt)?;
741                consequent.write_logic(&mut c, registry, interner, fmt)?;
742                write!(w, "{}", fmt.counterfactual(&a, &c))
743            }
744
745            LogicExpr::Causal { effect, cause } => {
746                write!(w, "Cause(")?;
747                cause.write_logic(w, registry, interner, fmt)?;
748                write!(w, ", ")?;
749                effect.write_logic(w, registry, interner, fmt)?;
750                write!(w, ")")
751            }
752
753            LogicExpr::Concessive { main, concession } => {
754                // main ∧ Concessive(concession): the main holds despite the concession.
755                main.write_logic(w, registry, interner, fmt)?;
756                write!(w, " {} Concessive(", fmt.and())?;
757                concession.write_logic(w, registry, interner, fmt)?;
758                write!(w, ")")
759            }
760
761            LogicExpr::Comparative { adjective, subject, object, difference, relation } => {
762                use crate::ast::ComparisonRelation;
763                // Equatives render as a max-degree at-least/equality comparison:
764                // "John is as tall as Mary." → max{d:Tall(John,d)} ≥ max{d:Tall(Mary,d)}.
765                if !matches!(relation, ComparisonRelation::Greater) {
766                    let rel = if matches!(relation, ComparisonRelation::Equal) { "=" } else { "≥" };
767                    let adj = interner.resolve(*adjective);
768                    let mut s = String::new();
769                    subject.write_to(&mut s, registry, interner)?;
770                    let mut o = String::new();
771                    object.write_to(&mut o, registry, interner)?;
772                    let cap = adj.chars().next()
773                        .map(|c| c.to_uppercase().collect::<String>() + &adj[1..])
774                        .unwrap_or_default();
775                    return write!(w, "max{{d:{}({},d)}} {} max{{d:{}({},d)}}", cap, s, rel, cap, o);
776                }
777                let adj = interner.resolve(*adjective);
778                let mut subj_buf = String::new();
779                if fmt.preserve_case() {
780                    subject.write_to_raw(&mut subj_buf, interner)?;
781                } else if fmt.use_full_names() {
782                    subject.write_to_full(&mut subj_buf, registry, interner)?;
783                } else {
784                    subject.write_to(&mut subj_buf, registry, interner)?;
785                }
786                let mut obj_buf = String::new();
787                if fmt.preserve_case() {
788                    object.write_to_raw(&mut obj_buf, interner)?;
789                } else if fmt.use_full_names() {
790                    object.write_to_full(&mut obj_buf, registry, interner)?;
791                } else {
792                    object.write_to(&mut obj_buf, registry, interner)?;
793                }
794                let diff_str = if let Some(diff) = difference {
795                    let mut diff_buf = String::new();
796                    if fmt.preserve_case() {
797                        diff.write_to_raw(&mut diff_buf, interner)?;
798                    } else if fmt.use_full_names() {
799                        diff.write_to_full(&mut diff_buf, registry, interner)?;
800                    } else {
801                        diff.write_to(&mut diff_buf, registry, interner)?;
802                    }
803                    Some(diff_buf)
804                } else {
805                    None
806                };
807                fmt.write_comparative(w, adj, &subj_buf, &obj_buf, diff_str.as_deref())
808            }
809
810            LogicExpr::Superlative { adjective, subject, domain } => {
811                let mut s = String::new();
812                subject.write_to(&mut s, registry, interner)?;
813                let mut d = String::new();
814                write_capitalized(&mut d, interner.resolve(*domain))?;
815                let comp = format!("{}er", interner.resolve(*adjective));
816                write!(w, "{}", fmt.superlative(&comp, &d, &s))
817            }
818
819            LogicExpr::Scopal { operator, body } => {
820                write!(w, "{}(", interner.resolve(*operator))?;
821                body.write_logic(w, registry, interner, fmt)?;
822                write!(w, ")")
823            }
824
825            LogicExpr::TemporalAnchor { anchor, body } => {
826                write!(w, "{}(", interner.resolve(*anchor))?;
827                body.write_logic(w, registry, interner, fmt)?;
828                write!(w, ")")
829            }
830
831            LogicExpr::Control { verb, subject, object, infinitive } => {
832                write!(w, "{}(", fmt.sanitize(&registry.get_symbol(*verb, interner)))?;
833                subject.write_to(w, registry, interner)?;
834                if let Some(obj) = object {
835                    write!(w, ", ")?;
836                    obj.write_to(w, registry, interner)?;
837                }
838                write!(w, ", ")?;
839                infinitive.write_logic(w, registry, interner, fmt)?;
840                write!(w, ")")
841            }
842
843            LogicExpr::Presupposition { assertion, presupposition } => {
844                assertion.write_logic(w, registry, interner, fmt)?;
845                write!(w, " [Presup: ")?;
846                presupposition.write_logic(w, registry, interner, fmt)?;
847                write!(w, "]")
848            }
849
850            LogicExpr::Focus { kind, focused, scope } => {
851                use crate::token::FocusKind;
852                let prefix = match kind {
853                    FocusKind::Only => "Only",
854                    FocusKind::Even => "Even",
855                    FocusKind::Just => "Just",
856                    FocusKind::Cleft => "Cleft",
857                };
858                write!(w, "{}(", prefix)?;
859                focused.write_to(w, registry, interner)?;
860                write!(w, ", ")?;
861                scope.write_logic(w, registry, interner, fmt)?;
862                write!(w, ")")
863            }
864
865            LogicExpr::Distributive { predicate } => {
866                write!(w, "*")?;
867                predicate.write_logic(w, registry, interner, fmt)
868            }
869
870            LogicExpr::GroupQuantifier { group_var, count, member_var, restriction, body } => {
871                let g = interner.resolve(*group_var);
872                let x = interner.resolve(*member_var);
873
874                // ∃g(Group(g) ∧ Count(g,n) ∧ ∀x(Member(x,g) → restriction) ∧ body)
875                write!(w, "{}{}(Group({}) {} Count({}, {}) {} {}{}(Member({}, {}) {} ",
876                    fmt.existential(), g, g,
877                    fmt.and(), g, count,
878                    fmt.and(), fmt.universal(), x, x, g, fmt.implies())?;
879
880                restriction.write_logic(w, registry, interner, fmt)?;
881
882                write!(w, ") {} ", fmt.and())?;
883
884                body.write_logic(w, registry, interner, fmt)?;
885
886                write!(w, ")")
887            }
888        }
889    }
890
891    /// Transpiles to a logic formula string using a custom formatter.
892    pub fn transpile_with<F: LogicFormatter>(
893        &self,
894        registry: &mut SymbolRegistry,
895        interner: &Interner,
896        fmt: &F,
897    ) -> String {
898        let mut buf = String::new();
899        let _ = self.write_logic(&mut buf, registry, interner, fmt);
900        buf
901    }
902
903    /// Transpiles to a logic formula string in the specified output format.
904    ///
905    /// # Formats
906    ///
907    /// - [`OutputFormat::Unicode`]: ∀x(Cat(x) → Sleeps(x))
908    /// - [`OutputFormat::LaTeX`]: `\forall x(Cat(x) \to Sleeps(x))`
909    /// - [`OutputFormat::SimpleFOL`]: Ax(Cat(x) -> Sleeps(x))
910    /// - [`OutputFormat::Kripke`]: □(P) @w0
911    pub fn transpile(
912        &self,
913        registry: &mut SymbolRegistry,
914        interner: &Interner,
915        format: OutputFormat,
916    ) -> String {
917        match format {
918            OutputFormat::Unicode => self.transpile_with(registry, interner, &UnicodeFormatter),
919            OutputFormat::LaTeX => self.transpile_with(registry, interner, &LatexFormatter),
920            OutputFormat::SimpleFOL => self.transpile_with(registry, interner, &SimpleFOLFormatter),
921            OutputFormat::Kripke => self.transpile_with(registry, interner, &KripkeFormatter),
922        }
923    }
924
925    /// Transpiles using a [`TranspileContext`] and custom formatter.
926    pub fn transpile_ctx<F: LogicFormatter>(
927        &self,
928        ctx: &mut TranspileContext<'_>,
929        fmt: &F,
930    ) -> String {
931        self.transpile_with(ctx.registry, ctx.interner, fmt)
932    }
933
934    /// Transpiles to Unicode format using a [`TranspileContext`].
935    pub fn transpile_ctx_unicode(&self, ctx: &mut TranspileContext<'_>) -> String {
936        self.transpile_ctx(ctx, &UnicodeFormatter)
937    }
938
939    /// Transpiles to LaTeX format using a [`TranspileContext`].
940    pub fn transpile_ctx_latex(&self, ctx: &mut TranspileContext<'_>) -> String {
941        self.transpile_ctx(ctx, &LatexFormatter)
942    }
943}