Skip to main content

logicaffeine_language/
proof_convert.rs

1//! Conversion from parser AST to proof engine representation.
2//!
3//! This module bridges the parser's arena-allocated AST ([`LogicExpr<'a>`]) to the
4//! proof engine's owned representation ([`ProofExpr`]).
5//!
6//! The conversion clones all data into owned Strings, enabling proof trees
7//! to persist beyond the arena's lifetime. Symbols are resolved using the
8//! interner at conversion time.
9//!
10//! # Key Function
11//!
12//! [`logic_expr_to_proof_expr`] is the main entry point for converting
13//! parsed expressions to the format expected by the proof search engine.
14
15use crate::ast::logic::{
16    BinaryTemporalOp, LogicExpr, ModalDomain, ModalFlavor, QuantifierKind, TemporalOperator, Term,
17    ThematicRole,
18};
19use crate::intern::Interner;
20use crate::lexicon::get_canonical_noun;
21use logicaffeine_proof::{ProofExpr, ProofTerm};
22use crate::token::TokenType;
23
24// =============================================================================
25// PUBLIC API
26// =============================================================================
27
28/// Map the parser's comparison-predicate names onto the proof oracle's canonical,
29/// case-sensitive vocabulary (`Gt`/`Lt`/`Gte`/`Lte`/`Eq`/`Neq`; see oracle.rs:879
30/// and modal_translation.rs:124). Returns `None` for any non-comparison name,
31/// which then falls through to ordinary noun normalization. The parser only ever
32/// emits these names for binary degree comparisons, so the caller gates on arity 2.
33fn canonical_comparison_name(name: &str) -> Option<&'static str> {
34    match name {
35        "Greater" | "Gt" => Some("Gt"),
36        "Less" | "Lt" => Some("Lt"),
37        "GreaterEqual" | "Gte" => Some("Gte"),
38        "LessEqual" | "Lte" => Some("Lte"),
39        "Equal" | "Eq" => Some("Eq"),
40        "NotEqual" | "Neq" => Some("Neq"),
41        _ => None,
42    }
43}
44
45/// Map the parser's arithmetic-function names onto the oracle's canonical,
46/// case-sensitive `Add`/`Sub`/`Mul`/`Div` (see oracle.rs:1034). Returns `None`
47/// for every other function name so measure functions (`Score`, `Ord`, …) stay
48/// verbatim as uninterpreted integer functions.
49fn canonical_arithmetic_fn(name: &str) -> Option<&'static str> {
50    match name {
51        "add" | "Add" => Some("Add"),
52        "sub" | "Sub" => Some("Sub"),
53        "mul" | "Mul" => Some("Mul"),
54        "div" | "Div" => Some("Div"),
55        _ => None,
56    }
57}
58
59/// Rename a free variable `from` → `to` inside a [`ProofTerm`].
60fn subst_proof_term(t: &ProofTerm, from: &str, to: &str) -> ProofTerm {
61    match t {
62        ProofTerm::Variable(v) if v == from => ProofTerm::Variable(to.to_string()),
63        ProofTerm::BoundVarRef(v) if v == from => ProofTerm::BoundVarRef(to.to_string()),
64        ProofTerm::Function(name, args) => ProofTerm::Function(
65            name.clone(),
66            args.iter().map(|a| subst_proof_term(a, from, to)).collect(),
67        ),
68        ProofTerm::Group(args) => {
69            ProofTerm::Group(args.iter().map(|a| subst_proof_term(a, from, to)).collect())
70        }
71        other => other.clone(),
72    }
73}
74
75/// Rename a free variable `from` → `to` inside a [`ProofExpr`], stopping at a
76/// quantifier that re-binds `from` (capture avoidance). Used to build the
77/// uniqueness clause of an "exactly one" expansion.
78fn subst_proof_var(e: &ProofExpr, from: &str, to: &str) -> ProofExpr {
79    match e {
80        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
81            name: name.clone(),
82            args: args.iter().map(|a| subst_proof_term(a, from, to)).collect(),
83            world: world.clone(),
84        },
85        ProofExpr::Identity(a, b) => {
86            ProofExpr::Identity(subst_proof_term(a, from, to), subst_proof_term(b, from, to))
87        }
88        ProofExpr::And(l, r) => ProofExpr::And(
89            Box::new(subst_proof_var(l, from, to)),
90            Box::new(subst_proof_var(r, from, to)),
91        ),
92        ProofExpr::Or(l, r) => ProofExpr::Or(
93            Box::new(subst_proof_var(l, from, to)),
94            Box::new(subst_proof_var(r, from, to)),
95        ),
96        ProofExpr::Implies(l, r) => ProofExpr::Implies(
97            Box::new(subst_proof_var(l, from, to)),
98            Box::new(subst_proof_var(r, from, to)),
99        ),
100        ProofExpr::Iff(l, r) => ProofExpr::Iff(
101            Box::new(subst_proof_var(l, from, to)),
102            Box::new(subst_proof_var(r, from, to)),
103        ),
104        ProofExpr::Not(x) => ProofExpr::Not(Box::new(subst_proof_var(x, from, to))),
105        ProofExpr::ForAll { variable, body } if variable != from => ProofExpr::ForAll {
106            variable: variable.clone(),
107            body: Box::new(subst_proof_var(body, from, to)),
108        },
109        ProofExpr::Exists { variable, body } if variable != from => ProofExpr::Exists {
110            variable: variable.clone(),
111            body: Box::new(subst_proof_var(body, from, to)),
112        },
113        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
114            operator: operator.clone(),
115            body: Box::new(subst_proof_var(body, from, to)),
116        },
117        ProofExpr::Term(t) => ProofExpr::Term(subst_proof_term(t, from, to)),
118        other => other.clone(),
119    }
120}
121
122fn subst_term_with(t: &ProofTerm, from: &str, to: &ProofTerm) -> ProofTerm {
123    match t {
124        ProofTerm::Variable(v) if v == from => to.clone(),
125        ProofTerm::BoundVarRef(v) if v == from => to.clone(),
126        ProofTerm::Function(name, args) => ProofTerm::Function(
127            name.clone(),
128            args.iter().map(|a| subst_term_with(a, from, to)).collect(),
129        ),
130        ProofTerm::Group(args) => {
131            ProofTerm::Group(args.iter().map(|a| subst_term_with(a, from, to)).collect())
132        }
133        other => other.clone(),
134    }
135}
136
137/// Substitute a free variable `from` with the CONSTANT `to` inside a [`ProofExpr`]
138/// — the operation that turns a wh-question body φ(x) into a candidate goal φ(c),
139/// so "who/what is …?" is answered by enumerating domain individuals and proving
140/// each candidate. Stops at a quantifier that re-binds `from` (capture avoidance).
141pub fn instantiate_var_with_constant(e: &ProofExpr, from: &str, to: &str) -> ProofExpr {
142    let c = ProofTerm::Constant(to.to_string());
143    subst_expr_with(e, from, &c)
144}
145
146fn subst_expr_with(e: &ProofExpr, from: &str, to: &ProofTerm) -> ProofExpr {
147    match e {
148        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
149            name: name.clone(),
150            args: args.iter().map(|a| subst_term_with(a, from, to)).collect(),
151            world: world.clone(),
152        },
153        ProofExpr::Identity(a, b) => {
154            ProofExpr::Identity(subst_term_with(a, from, to), subst_term_with(b, from, to))
155        }
156        ProofExpr::And(l, r) => ProofExpr::And(
157            Box::new(subst_expr_with(l, from, to)),
158            Box::new(subst_expr_with(r, from, to)),
159        ),
160        ProofExpr::Or(l, r) => ProofExpr::Or(
161            Box::new(subst_expr_with(l, from, to)),
162            Box::new(subst_expr_with(r, from, to)),
163        ),
164        ProofExpr::Implies(l, r) => ProofExpr::Implies(
165            Box::new(subst_expr_with(l, from, to)),
166            Box::new(subst_expr_with(r, from, to)),
167        ),
168        ProofExpr::Iff(l, r) => ProofExpr::Iff(
169            Box::new(subst_expr_with(l, from, to)),
170            Box::new(subst_expr_with(r, from, to)),
171        ),
172        ProofExpr::Not(x) => ProofExpr::Not(Box::new(subst_expr_with(x, from, to))),
173        ProofExpr::ForAll { variable, body } if variable != from => ProofExpr::ForAll {
174            variable: variable.clone(),
175            body: Box::new(subst_expr_with(body, from, to)),
176        },
177        ProofExpr::Exists { variable, body } if variable != from => ProofExpr::Exists {
178            variable: variable.clone(),
179            body: Box::new(subst_expr_with(body, from, to)),
180        },
181        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
182            operator: operator.clone(),
183            body: Box::new(subst_expr_with(body, from, to)),
184        },
185        ProofExpr::Term(t) => ProofExpr::Term(subst_term_with(t, from, to)),
186        other => other.clone(),
187    }
188}
189
190/// Convert a LogicExpr to ProofExpr.
191///
192/// This is the main entry point for bridging the parser to the proof engine.
193/// All Symbols are resolved to owned Strings using the interner.
194pub fn logic_expr_to_proof_expr<'a>(expr: &LogicExpr<'a>, interner: &Interner) -> ProofExpr {
195    match expr {
196        // --- Core FOL ---
197        LogicExpr::Predicate { name, args, world } => {
198            // Semantic Normalization:
199            // 1. Lemmatize: "cats" → "Cat", "men" → "Man" (canonical noun form)
200            // 2. Lowercase: "Cat" → "cat", "Mortal" → "mortal"
201            // This ensures "Mortal" (noun) == "mortal" (adj) == "mortals" (plural noun)
202            let name_str = interner.resolve(*name);
203            // A binary comparison predicate carries arithmetic meaning the oracle
204            // recognises only under its canonical name; it bypasses noun
205            // normalization (which would lowercase "Greater" → "greater" and strand
206            // the comparison as an uninterpreted function the solver cannot use).
207            let normalized = match (args.len() == 2)
208                .then(|| canonical_comparison_name(name_str))
209                .flatten()
210            {
211                Some(canon) => canon.to_string(),
212                None => get_canonical_noun(&name_str.to_lowercase())
213                    .map(|lemma| lemma.to_lowercase())
214                    .unwrap_or_else(|| name_str.to_lowercase()),
215            };
216
217            ProofExpr::Predicate {
218                name: normalized,
219                args: args.iter().map(|t| term_to_proof_term(t, interner)).collect(),
220                world: world.map(|w| interner.resolve(w).to_string()),
221            }
222        }
223
224        LogicExpr::Identity { left, right } => ProofExpr::Identity(
225            term_to_proof_term(left, interner),
226            term_to_proof_term(right, interner),
227        ),
228
229        LogicExpr::Atom(s) => ProofExpr::Atom(interner.resolve(*s).to_string()),
230
231        // --- Quantifiers ---
232        LogicExpr::Quantifier {
233            kind,
234            variable,
235            body,
236            ..
237        } => {
238            let var_name = interner.resolve(*variable).to_string();
239            let body_expr = Box::new(logic_expr_to_proof_expr(body, interner));
240
241            match kind {
242                QuantifierKind::Universal => ProofExpr::ForAll {
243                    variable: var_name,
244                    body: body_expr,
245                },
246                QuantifierKind::Existential => ProofExpr::Exists {
247                    variable: var_name,
248                    body: body_expr,
249                },
250                // Map other quantifiers to existential with a note
251                QuantifierKind::Most => ProofExpr::Unsupported("Most quantifier".into()),
252                QuantifierKind::Few => ProofExpr::Unsupported("Few quantifier".into()),
253                QuantifierKind::Many => ProofExpr::Unsupported("Many quantifier".into()),
254                QuantifierKind::Generic => ProofExpr::ForAll {
255                    variable: var_name,
256                    body: body_expr,
257                },
258                QuantifierKind::Cardinal(1) => {
259                    // "Exactly one x: φ(x)" = ∃x(φ(x) ∧ ∀y(φ(y) → y = x)) — existence
260                    // PLUS uniqueness, the form the entailment oracle reasons over.
261                    // Dropping the count (plain ∃) loses the constraint a logic-grid
262                    // bijection depends on.
263                    let x = var_name;
264                    let y = format!("{x}_uniq");
265                    let phi_y = Box::new(subst_proof_var(&body_expr, &x, &y));
266                    let uniqueness = ProofExpr::ForAll {
267                        variable: y.clone(),
268                        body: Box::new(ProofExpr::Implies(
269                            phi_y,
270                            Box::new(ProofExpr::Identity(
271                                ProofTerm::Variable(y),
272                                ProofTerm::Variable(x.clone()),
273                            )),
274                        )),
275                    };
276                    ProofExpr::Exists {
277                        variable: x,
278                        body: Box::new(ProofExpr::And(body_expr, Box::new(uniqueness))),
279                    }
280                }
281                QuantifierKind::Cardinal(n) => {
282                    // n ≥ 2: existence is a sound (if incomplete) weakening of the
283                    // exact count for the proof path.
284                    ProofExpr::Exists {
285                        variable: format!("{}_{}", var_name, n),
286                        body: body_expr,
287                    }
288                }
289                QuantifierKind::AtLeast(_) | QuantifierKind::AtMost(_) => {
290                    ProofExpr::Unsupported("Counting quantifier".into())
291                }
292            }
293        }
294
295        // --- Logical Connectives ---
296        LogicExpr::BinaryOp { left, op, right } => {
297            let l = Box::new(logic_expr_to_proof_expr(left, interner));
298            let r = Box::new(logic_expr_to_proof_expr(right, interner));
299
300            match op {
301                TokenType::And => ProofExpr::And(l, r),
302                TokenType::Or => ProofExpr::Or(l, r),
303                TokenType::If | TokenType::Implies | TokenType::Then => ProofExpr::Implies(l, r),
304                TokenType::Iff => ProofExpr::Iff(l, r),
305                _ => ProofExpr::Unsupported(format!("Binary operator {:?}", op)),
306            }
307        }
308
309        LogicExpr::UnaryOp { op, operand } => {
310            let inner = Box::new(logic_expr_to_proof_expr(operand, interner));
311            match op {
312                TokenType::Not => ProofExpr::Not(inner),
313                _ => ProofExpr::Unsupported(format!("Unary operator {:?}", op)),
314            }
315        }
316
317        // --- Modal Logic ---
318        LogicExpr::Modal { vector, operand } => {
319            let body = Box::new(logic_expr_to_proof_expr(operand, interner));
320            let domain = match vector.domain {
321                ModalDomain::Alethic => "Alethic",
322                ModalDomain::Deontic => "Deontic",
323                ModalDomain::Temporal => "Temporal",
324            };
325            let flavor = match vector.flavor {
326                ModalFlavor::Root => "Root",
327                ModalFlavor::Epistemic => "Epistemic",
328                ModalFlavor::Evidential => "Evidential",
329                ModalFlavor::Bouletic => "Bouletic",
330            };
331            ProofExpr::Modal {
332                domain: domain.to_string(),
333                force: vector.force,
334                flavor: flavor.to_string(),
335                body,
336            }
337        }
338
339        // --- Temporal Logic ---
340        LogicExpr::Temporal { operator, body } => {
341            let body_expr = Box::new(logic_expr_to_proof_expr(body, interner));
342            let op_name = match operator {
343                TemporalOperator::Past => "Past",
344                TemporalOperator::Future => "Future",
345                TemporalOperator::Always => "Always",
346                TemporalOperator::Eventually
347                | TemporalOperator::BoundedEventually(_) => "Eventually",
348                TemporalOperator::Next => "Next",
349            };
350            ProofExpr::Temporal {
351                operator: op_name.to_string(),
352                body: body_expr,
353            }
354        }
355
356        LogicExpr::TemporalBinary { operator, left, right } => ProofExpr::TemporalBinary {
357            operator: format!("{:?}", operator),
358            left: Box::new(logic_expr_to_proof_expr(left, interner)),
359            right: Box::new(logic_expr_to_proof_expr(right, interner)),
360        },
361
362        // --- Lambda Calculus ---
363        LogicExpr::Lambda { variable, body } => ProofExpr::Lambda {
364            variable: interner.resolve(*variable).to_string(),
365            body: Box::new(logic_expr_to_proof_expr(body, interner)),
366        },
367
368        LogicExpr::App { function, argument } => ProofExpr::App(
369            Box::new(logic_expr_to_proof_expr(function, interner)),
370            Box::new(logic_expr_to_proof_expr(argument, interner)),
371        ),
372
373        // --- Event Semantics ---
374        LogicExpr::NeoEvent(data) => {
375            let roles: Vec<(String, ProofTerm)> = data
376                .roles
377                .iter()
378                .map(|(role, term)| {
379                    let role_name = match role {
380                        ThematicRole::Agent => "Agent",
381                        ThematicRole::Patient => "Patient",
382                        ThematicRole::Theme => "Theme",
383                        ThematicRole::Recipient => "Recipient",
384                        ThematicRole::Goal => "Goal",
385                        ThematicRole::Source => "Source",
386                        ThematicRole::Instrument => "Instrument",
387                        ThematicRole::Location => "Location",
388                        ThematicRole::Time => "Time",
389                        ThematicRole::Manner => "Manner",
390                        ThematicRole::Result => "Result",
391                        ThematicRole::Depictive => "Depictive",
392                    };
393                    (role_name.to_string(), term_to_proof_term(term, interner))
394                })
395                .collect();
396
397            ProofExpr::NeoEvent {
398                event_var: interner.resolve(data.event_var).to_string(),
399                verb: interner.resolve(data.verb).to_string(),
400                roles,
401            }
402        }
403
404        // --- Counterfactual ---
405        LogicExpr::Counterfactual {
406            antecedent,
407            consequent,
408        } => {
409            // □→ keeps closest-world semantics: the consequent is quantified
410            // over the similarity-closest antecedent-worlds, never lowered to
411            // material implication (§4.5).
412            ProofExpr::Counterfactual {
413                antecedent: Box::new(logic_expr_to_proof_expr(antecedent, interner)),
414                consequent: Box::new(logic_expr_to_proof_expr(consequent, interner)),
415            }
416        }
417
418        // --- Unsupported constructs (return Unsupported variant) ---
419        LogicExpr::Categorical(_) => ProofExpr::Unsupported("Categorical (legacy)".into()),
420        LogicExpr::Relation(_) => ProofExpr::Unsupported("Relation (legacy)".into()),
421        LogicExpr::Metaphor { .. } => ProofExpr::Unsupported("Metaphor".into()),
422        // A wh-question "Who is a lawyer?" is the GOAL ∃x.φ(x): proving it means
423        // SOMEONE satisfies φ; the ANSWER is the witness (extracted by enumerating
424        // the domain in `answer_question`). Carrying the variable + body lets the
425        // answer layer recover both.
426        LogicExpr::Question { wh_variable, body } => ProofExpr::Exists {
427            variable: interner.resolve(*wh_variable).to_string(),
428            body: Box::new(logic_expr_to_proof_expr(body, interner)),
429        },
430        LogicExpr::YesNoQuestion { .. } => ProofExpr::Unsupported("YesNoQuestion".into()),
431        LogicExpr::Intensional { .. } => ProofExpr::Unsupported("Intensional".into()),
432        LogicExpr::Event { .. } => ProofExpr::Unsupported("Event (legacy)".into()),
433        LogicExpr::Imperative { action } => {
434            // Directive(h, p) → O_g p: the commanded action becomes a bouletic
435            // obligation over the addressee's action worlds (§1.4). The action
436            // itself is NOT asserted — commanding is not doing.
437            ProofExpr::Modal {
438                domain: "Deontic".to_string(),
439                force: 1.0,
440                flavor: "Bouletic".to_string(),
441                body: Box::new(logic_expr_to_proof_expr(action, interner)),
442            }
443        }
444        LogicExpr::Exclamative { body, .. } => {
445            // The presupposed/asserted content is the body predication.
446            logic_expr_to_proof_expr(body, interner)
447        }
448        LogicExpr::Optative { wish } => {
449            // Wish(speaker, ⟨p⟩): a bouletic necessity over the speaker's
450            // preference-ideal worlds (§1.2); the complement is not entailed.
451            ProofExpr::Modal {
452                domain: "Deontic".to_string(),
453                force: 1.0,
454                flavor: "Bouletic".to_string(),
455                body: Box::new(logic_expr_to_proof_expr(wish, interner)),
456            }
457        }
458        LogicExpr::Implicature { assertion, .. } => {
459            // Truth-conditional content is the literal assertion; the implicature is
460            // defeasible/cancellable and not part of the entailment core.
461            logic_expr_to_proof_expr(assertion, interner)
462        }
463        LogicExpr::SpeechAct { performer, act_type, .. } => {
464            // A performative asserts that the act is performed at the utterance
465            // world (the saying is the doing): `act_type(performer)`. The
466            // propositional content is NOT asserted — promising to φ does not make
467            // φ true — so it is deliberately not conjoined here.
468            ProofExpr::Predicate {
469                name: interner.resolve(*act_type).to_lowercase(),
470                args: vec![term_to_proof_term(&Term::Constant(*performer), interner)],
471                world: None,
472            }
473        }
474        LogicExpr::Causal { .. } => ProofExpr::Unsupported("Causal".into()),
475        LogicExpr::Concessive { main, .. } => {
476            // The main clause is asserted; the concession is backgrounded (a defeated
477            // expectation), so the truth-conditional content reduces to the main.
478            logic_expr_to_proof_expr(main, interner)
479        }
480        LogicExpr::Comparative { .. } => ProofExpr::Unsupported("Comparative".into()),
481        LogicExpr::Superlative { .. } => ProofExpr::Unsupported("Superlative".into()),
482        LogicExpr::Scopal { .. } => ProofExpr::Unsupported("Scopal".into()),
483        LogicExpr::Control { .. } => ProofExpr::Unsupported("Control".into()),
484        LogicExpr::Presupposition {
485            assertion,
486            presupposition,
487        } => {
488            // A surviving (projected/accommodated) presupposition is real
489            // content: "Mary doesn't regret lying" carries both ¬Regret and
490            // the projected Lied(mary). Bound/filtered presuppositions are
491            // rewritten away before this point (Van der Sandt pass), so the
492            // conjunction is monotonically sound.
493            ProofExpr::And(
494                Box::new(logic_expr_to_proof_expr(assertion, interner)),
495                Box::new(logic_expr_to_proof_expr(presupposition, interner)),
496            )
497        }
498        LogicExpr::Focus { scope, .. } => {
499            // Focus marking is information structure; the truth-conditional
500            // content is the scope. Cleft exhaustivity is already a separate
501            // conjunct built by the parser.
502            logic_expr_to_proof_expr(scope, interner)
503        }
504        LogicExpr::TemporalAnchor { .. } => ProofExpr::Unsupported("TemporalAnchor".into()),
505        LogicExpr::Distributive { predicate } => {
506            // *P(σN) — atomic distribution over the plural sum. Members of
507            // σN are exactly the Ns (Link lattice atoms), so the first-order
508            // form is ∀x(N(x) → P(x)).
509            let base = logic_expr_to_proof_expr(predicate, interner);
510            match find_sigma_symbol(predicate) {
511                Some(noun) => {
512                    let noun_str = interner.resolve(noun).to_string();
513                    let noun_pred = get_canonical_noun(&noun_str.to_lowercase())
514                        .map(|lemma| lemma.to_lowercase())
515                        .unwrap_or_else(|| noun_str.to_lowercase());
516                    let var = format!("each_{}", noun_pred);
517                    let sigma_term = ProofTerm::Variable(noun_str);
518                    let member = ProofTerm::Variable(var.clone());
519                    let body = replace_proof_term(&base, &sigma_term, &member);
520                    ProofExpr::ForAll {
521                        variable: var.clone(),
522                        body: Box::new(ProofExpr::Implies(
523                            Box::new(ProofExpr::Predicate {
524                                name: noun_pred,
525                                args: vec![ProofTerm::Variable(var)],
526                                world: None,
527                            }),
528                            Box::new(body),
529                        )),
530                    }
531                }
532                None => base,
533            }
534        }
535        LogicExpr::GroupQuantifier {
536            group_var,
537            count,
538            member_var,
539            restriction,
540            body,
541        } => {
542            // ∃g(group(g) ∧ count(g, n) ∧ ∀x(member(x, g) → R(x)) ∧ B(g))
543            let g = interner.resolve(*group_var).to_string();
544            let x = interner.resolve(*member_var).to_string();
545            let group_pred = ProofExpr::Predicate {
546                name: "group".to_string(),
547                args: vec![ProofTerm::Variable(g.clone())],
548                world: None,
549            };
550            let count_pred = ProofExpr::Predicate {
551                name: "count".to_string(),
552                args: vec![
553                    ProofTerm::Variable(g.clone()),
554                    ProofTerm::Constant(count.to_string()),
555                ],
556                world: None,
557            };
558            let member_pred = ProofExpr::Predicate {
559                name: "member".to_string(),
560                args: vec![ProofTerm::Variable(x.clone()), ProofTerm::Variable(g.clone())],
561                world: None,
562            };
563            let members = ProofExpr::ForAll {
564                variable: x,
565                body: Box::new(ProofExpr::Implies(
566                    Box::new(member_pred),
567                    Box::new(logic_expr_to_proof_expr(restriction, interner)),
568                )),
569            };
570            ProofExpr::Exists {
571                variable: g,
572                body: Box::new(ProofExpr::And(
573                    Box::new(ProofExpr::And(
574                        Box::new(ProofExpr::And(Box::new(group_pred), Box::new(count_pred))),
575                        Box::new(members),
576                    )),
577                    Box::new(logic_expr_to_proof_expr(body, interner)),
578                )),
579            }
580        }
581        // Aspectual wrappers (Imperfective, Perfective, etc.) are transparent to proof.
582        // "John runs" -> Aspectual(Imperfective, ∃e(Run(e) ∧ Agent(e, John)))
583        // We pass through to the inner event structure.
584        LogicExpr::Aspectual { body, .. } => logic_expr_to_proof_expr(body, interner),
585        LogicExpr::Voice { .. } => ProofExpr::Unsupported("Voice".into()),
586    }
587}
588
589/// Convert a Term to ProofTerm.
590/// A defeasible default extracted by [`logic_expr_to_proof_expr_defeasible`]:
591/// the abnormality predicate guarding one GEN rule or implicature.
592#[derive(Debug, Clone)]
593pub struct DefaultRule {
594    /// The abnormality predicate name (`ab_1`, `ab_2`, …).
595    pub ab_name: String,
596    /// The generic's restrictor predicate ("penguin" in GEN x(Penguin → …)),
597    /// used for specificity ordering. `None` for implicatures.
598    pub restriction_pred: Option<String>,
599    /// Unary (per-individual, generics) vs propositional (implicatures).
600    pub unary: bool,
601}
602
603/// Convert with DEFEASIBLE semantics: a generic `GEN x(R(x) → N(x))` becomes
604/// the abnormality-guarded `∀x((R(x) ∧ ¬ab_k(x)) → N(x))`, and an implicature
605/// is asserted under its own guard (`assertion ∧ (¬ab_k → implicature)`).
606/// The circumscription itself — minimizing each `ab_k` — happens in the
607/// defeasible reasoner; this conversion only preserves what the strict
608/// export (`Generic → ∀`, implicature dropped) erases.
609pub fn logic_expr_to_proof_expr_defeasible<'a>(
610    expr: &LogicExpr<'a>,
611    interner: &Interner,
612    defaults: &mut Vec<DefaultRule>,
613) -> ProofExpr {
614    match expr {
615        LogicExpr::Quantifier {
616            kind: QuantifierKind::Generic,
617            variable,
618            body,
619            ..
620        } => {
621            let var = interner.resolve(*variable).to_string();
622            if let LogicExpr::BinaryOp {
623                left: restriction,
624                op: TokenType::Implies | TokenType::If,
625                right: nucleus,
626            } = body
627            {
628                let ab_name = format!("ab_{}", defaults.len() + 1);
629                let restriction_pred = match restriction {
630                    LogicExpr::Predicate { name, .. } => {
631                        let s = interner.resolve(*name);
632                        Some(
633                            get_canonical_noun(&s.to_lowercase())
634                                .map(|l| l.to_lowercase())
635                                .unwrap_or_else(|| s.to_lowercase()),
636                        )
637                    }
638                    _ => None,
639                };
640                defaults.push(DefaultRule {
641                    ab_name: ab_name.clone(),
642                    restriction_pred,
643                    unary: true,
644                });
645                let guarded = ProofExpr::And(
646                    Box::new(logic_expr_to_proof_expr(restriction, interner)),
647                    Box::new(ProofExpr::Not(Box::new(ProofExpr::Predicate {
648                        name: ab_name,
649                        args: vec![ProofTerm::Variable(var.clone())],
650                        world: None,
651                    }))),
652                );
653                return ProofExpr::ForAll {
654                    variable: var,
655                    body: Box::new(ProofExpr::Implies(
656                        Box::new(guarded),
657                        Box::new(logic_expr_to_proof_expr(nucleus, interner)),
658                    )),
659                };
660            }
661            logic_expr_to_proof_expr(expr, interner)
662        }
663        LogicExpr::Implicature {
664            assertion,
665            implicature,
666        } => {
667            let ab_name = format!("ab_{}", defaults.len() + 1);
668            defaults.push(DefaultRule {
669                ab_name: ab_name.clone(),
670                restriction_pred: None,
671                unary: false,
672            });
673            ProofExpr::And(
674                Box::new(logic_expr_to_proof_expr(assertion, interner)),
675                Box::new(ProofExpr::Implies(
676                    Box::new(ProofExpr::Not(Box::new(ProofExpr::Atom(ab_name)))),
677                    Box::new(logic_expr_to_proof_expr(implicature, interner)),
678                )),
679            )
680        }
681        // Containers recurse so defaults nested under connectives or
682        // presuppositions are still found.
683        LogicExpr::BinaryOp { left, op, right } => {
684            let l = logic_expr_to_proof_expr_defeasible(left, interner, defaults);
685            let r = logic_expr_to_proof_expr_defeasible(right, interner, defaults);
686            match op {
687                TokenType::And => ProofExpr::And(Box::new(l), Box::new(r)),
688                TokenType::Or => ProofExpr::Or(Box::new(l), Box::new(r)),
689                TokenType::If | TokenType::Implies => {
690                    ProofExpr::Implies(Box::new(l), Box::new(r))
691                }
692                TokenType::Iff => ProofExpr::Iff(Box::new(l), Box::new(r)),
693                _ => logic_expr_to_proof_expr(expr, interner),
694            }
695        }
696        LogicExpr::UnaryOp {
697            op: TokenType::Not,
698            operand,
699        } => ProofExpr::Not(Box::new(logic_expr_to_proof_expr_defeasible(
700            operand, interner, defaults,
701        ))),
702        LogicExpr::Presupposition {
703            assertion,
704            presupposition,
705        } => ProofExpr::And(
706            Box::new(logic_expr_to_proof_expr_defeasible(
707                assertion, interner, defaults,
708            )),
709            Box::new(logic_expr_to_proof_expr_defeasible(
710                presupposition,
711                interner,
712                defaults,
713            )),
714        ),
715        _ => logic_expr_to_proof_expr(expr, interner),
716    }
717}
718
719/// Find the first `Term::Sigma` symbol in an expression (the plural sum a
720/// `Distributive` operator ranges over).
721fn find_sigma_symbol<'a>(expr: &LogicExpr<'a>) -> Option<crate::Symbol> {
722    fn in_term<'a>(term: &Term<'a>) -> Option<crate::Symbol> {
723        match term {
724            Term::Sigma(s) => Some(*s),
725            Term::Function(_, args) | Term::Group(args) => args.iter().find_map(in_term),
726            Term::Possessed { possessor, .. } => in_term(possessor),
727            _ => None,
728        }
729    }
730    match expr {
731        LogicExpr::Predicate { args, .. } => args.iter().find_map(in_term),
732        LogicExpr::NeoEvent(data) => data.roles.iter().find_map(|(_, t)| in_term(t)),
733        LogicExpr::Quantifier { body, .. } => find_sigma_symbol(body),
734        LogicExpr::BinaryOp { left, right, .. } => {
735            find_sigma_symbol(left).or_else(|| find_sigma_symbol(right))
736        }
737        LogicExpr::UnaryOp { operand, .. } => find_sigma_symbol(operand),
738        LogicExpr::Modal { operand, .. } => find_sigma_symbol(operand),
739        LogicExpr::Temporal { body, .. } => find_sigma_symbol(body),
740        LogicExpr::Aspectual { body, .. } => find_sigma_symbol(body),
741        LogicExpr::Distributive { predicate } => find_sigma_symbol(predicate),
742        LogicExpr::Presupposition { assertion, .. } => find_sigma_symbol(assertion),
743        _ => None,
744    }
745}
746
747/// Replace every occurrence of `from` with `to` in the term positions of a
748/// proof expression (used to instantiate a plural sum by its members).
749fn replace_proof_term(expr: &ProofExpr, from: &ProofTerm, to: &ProofTerm) -> ProofExpr {
750    fn in_term(term: &ProofTerm, from: &ProofTerm, to: &ProofTerm) -> ProofTerm {
751        if term == from {
752            return to.clone();
753        }
754        match term {
755            ProofTerm::Function(name, args) => ProofTerm::Function(
756                name.clone(),
757                args.iter().map(|t| in_term(t, from, to)).collect(),
758            ),
759            ProofTerm::Group(terms) => {
760                ProofTerm::Group(terms.iter().map(|t| in_term(t, from, to)).collect())
761            }
762            other => other.clone(),
763        }
764    }
765    match expr {
766        ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
767            name: name.clone(),
768            args: args.iter().map(|t| in_term(t, from, to)).collect(),
769            world: world.clone(),
770        },
771        ProofExpr::Identity(l, r) => {
772            ProofExpr::Identity(in_term(l, from, to), in_term(r, from, to))
773        }
774        ProofExpr::And(l, r) => ProofExpr::And(
775            Box::new(replace_proof_term(l, from, to)),
776            Box::new(replace_proof_term(r, from, to)),
777        ),
778        ProofExpr::Or(l, r) => ProofExpr::Or(
779            Box::new(replace_proof_term(l, from, to)),
780            Box::new(replace_proof_term(r, from, to)),
781        ),
782        ProofExpr::Implies(l, r) => ProofExpr::Implies(
783            Box::new(replace_proof_term(l, from, to)),
784            Box::new(replace_proof_term(r, from, to)),
785        ),
786        ProofExpr::Iff(l, r) => ProofExpr::Iff(
787            Box::new(replace_proof_term(l, from, to)),
788            Box::new(replace_proof_term(r, from, to)),
789        ),
790        ProofExpr::Not(inner) => {
791            ProofExpr::Not(Box::new(replace_proof_term(inner, from, to)))
792        }
793        ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
794            variable: variable.clone(),
795            body: Box::new(replace_proof_term(body, from, to)),
796        },
797        ProofExpr::Exists { variable, body } => ProofExpr::Exists {
798            variable: variable.clone(),
799            body: Box::new(replace_proof_term(body, from, to)),
800        },
801        ProofExpr::Modal {
802            domain,
803            force,
804            flavor,
805            body,
806        } => ProofExpr::Modal {
807            domain: domain.clone(),
808            force: *force,
809            flavor: flavor.clone(),
810            body: Box::new(replace_proof_term(body, from, to)),
811        },
812        ProofExpr::Counterfactual {
813            antecedent,
814            consequent,
815        } => ProofExpr::Counterfactual {
816            antecedent: Box::new(replace_proof_term(antecedent, from, to)),
817            consequent: Box::new(replace_proof_term(consequent, from, to)),
818        },
819        ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
820            operator: operator.clone(),
821            body: Box::new(replace_proof_term(body, from, to)),
822        },
823        ProofExpr::NeoEvent {
824            event_var,
825            verb,
826            roles,
827        } => ProofExpr::NeoEvent {
828            event_var: event_var.clone(),
829            verb: verb.clone(),
830            roles: roles
831                .iter()
832                .map(|(r, t)| (r.clone(), in_term(t, from, to)))
833                .collect(),
834        },
835        other => other.clone(),
836    }
837}
838
839pub fn term_to_proof_term<'a>(term: &Term<'a>, interner: &Interner) -> ProofTerm {
840    match term {
841        Term::Constant(s) => ProofTerm::Constant(interner.resolve(*s).to_string()),
842
843        Term::Variable(s) => ProofTerm::Variable(interner.resolve(*s).to_string()),
844
845        Term::Function(name, args) => {
846            // An arithmetic offset ("add"/"sub") must reach the oracle under its
847            // canonical name (`Add`/`Sub`) or the equality degrades to an
848            // uninterpreted function and the offset never forces a value. Measure
849            // functions (Score, Ord, …) are left verbatim as uninterpreted ints.
850            let name_str = interner.resolve(*name);
851            let canon = (args.len() == 2)
852                .then(|| canonical_arithmetic_fn(name_str))
853                .flatten()
854                .map(|s| s.to_string())
855                .unwrap_or_else(|| name_str.to_string());
856            ProofTerm::Function(
857                canon,
858                args.iter().map(|t| term_to_proof_term(t, interner)).collect(),
859            )
860        }
861
862        Term::Group(terms) => {
863            ProofTerm::Group(terms.iter().map(|t| term_to_proof_term(t, interner)).collect())
864        }
865
866        Term::Possessed { possessor, possessed } => {
867            // Convert possession to function application: has(possessor, possessed)
868            ProofTerm::Function(
869                "has".to_string(),
870                vec![
871                    term_to_proof_term(possessor, interner),
872                    ProofTerm::Constant(interner.resolve(*possessed).to_string()),
873                ],
874            )
875        }
876
877        Term::Sigma(s) => {
878            // Sigma variables become regular variables
879            ProofTerm::Variable(interner.resolve(*s).to_string())
880        }
881
882        Term::Intension(s) => {
883            // Intensions become constants with ^ prefix
884            ProofTerm::Constant(format!("^{}", interner.resolve(*s)))
885        }
886
887        Term::Kind(s) => {
888            // Kind terms are reified entities; like intensions they become a
889            // ^-prefixed constant so kind predication reasons over a fixed object.
890            ProofTerm::Constant(format!("^{}", interner.resolve(*s)))
891        }
892
893        Term::Proposition(expr) => {
894            // Embedded propositions - convert recursively but wrap as constant
895            // This is a simplification; full handling would need reification
896            let proof_expr = logic_expr_to_proof_expr(expr, interner);
897            ProofTerm::Constant(format!("[{}]", proof_expr))
898        }
899
900        Term::Value { kind, unit, .. } => {
901            // Convert numeric values to constants
902            use crate::ast::logic::NumberKind;
903            match kind {
904                NumberKind::Integer(n) => {
905                    if let Some(u) = unit {
906                        ProofTerm::Constant(format!("{}{}", n, interner.resolve(*u)))
907                    } else {
908                        ProofTerm::Constant(n.to_string())
909                    }
910                }
911                NumberKind::Real(f) => {
912                    if let Some(u) = unit {
913                        ProofTerm::Constant(format!("{}{}", f, interner.resolve(*u)))
914                    } else {
915                        ProofTerm::Constant(f.to_string())
916                    }
917                }
918                NumberKind::Symbolic(s) => ProofTerm::Constant(interner.resolve(*s).to_string()),
919            }
920        }
921    }
922}
923
924#[cfg(test)]
925mod tests {
926    use super::*;
927    use crate::arena::Arena;
928
929    #[test]
930    fn test_convert_predicate() {
931        let mut interner = Interner::new();
932        let name = interner.intern("Man");
933        let arg = interner.intern("socrates");
934
935        let arena: Arena<Term> = Arena::new();
936        let args = arena.alloc_slice([Term::Constant(arg)]);
937
938        let expr = LogicExpr::Predicate {
939            name,
940            args,
941            world: None,
942        };
943
944        let result = logic_expr_to_proof_expr(&expr, &interner);
945
946        match result {
947            ProofExpr::Predicate { name, args, world } => {
948                // Predicate names are normalized to lowercase
949                assert_eq!(name, "man");
950                assert_eq!(args.len(), 1);
951                // Terms (constants) preserve their case
952                assert!(matches!(&args[0], ProofTerm::Constant(s) if s == "socrates"));
953                assert!(world.is_none());
954            }
955            _ => panic!("Expected Predicate, got {:?}", result),
956        }
957    }
958
959    #[test]
960    fn test_convert_universal() {
961        let mut interner = Interner::new();
962        let var = interner.intern("x");
963        let pred = interner.intern("P");
964
965        let arena: Arena<LogicExpr> = Arena::new();
966        let term_arena: Arena<Term> = Arena::new();
967
968        let body = arena.alloc(LogicExpr::Predicate {
969            name: pred,
970            args: term_arena.alloc_slice([Term::Variable(var)]),
971            world: None,
972        });
973
974        let expr = LogicExpr::Quantifier {
975            kind: QuantifierKind::Universal,
976            variable: var,
977            body,
978            island_id: 0,
979        };
980
981        let result = logic_expr_to_proof_expr(&expr, &interner);
982
983        match result {
984            ProofExpr::ForAll { variable, body } => {
985                assert_eq!(variable, "x");
986                assert!(matches!(*body, ProofExpr::Predicate { .. }));
987            }
988            _ => panic!("Expected ForAll, got {:?}", result),
989        }
990    }
991
992    #[test]
993    fn test_convert_implication() {
994        let mut interner = Interner::new();
995        let p = interner.intern("P");
996        let q = interner.intern("Q");
997
998        let arena: Arena<LogicExpr> = Arena::new();
999
1000        let left = arena.alloc(LogicExpr::Atom(p));
1001        let right = arena.alloc(LogicExpr::Atom(q));
1002
1003        let expr = LogicExpr::BinaryOp {
1004            left,
1005            op: TokenType::If,
1006            right,
1007        };
1008
1009        let result = logic_expr_to_proof_expr(&expr, &interner);
1010
1011        match result {
1012            ProofExpr::Implies(l, r) => {
1013                assert!(matches!(*l, ProofExpr::Atom(ref s) if s == "P"));
1014                assert!(matches!(*r, ProofExpr::Atom(ref s) if s == "Q"));
1015            }
1016            _ => panic!("Expected Implies, got {:?}", result),
1017        }
1018    }
1019
1020    // ---- Solver-canonical arithmetic vocabulary (the parser↔oracle bridge) ----
1021    //
1022    // The parser names comparison directions "Greater"/"Less"/… and arithmetic
1023    // offsets "add"/"sub", but the proof oracle recognises ONLY the canonical
1024    // "Gt"/"Lt"/"Gte"/"Lte"/"Eq"/"Neq" predicates and "Add"/"Sub"/"Mul"/"Div"
1025    // functions, matched case-sensitively (oracle.rs:879, 1034;
1026    // modal_translation.rs:124). If the bridge does not translate to the canonical
1027    // vocabulary, every arithmetic/comparison clue silently degrades to an
1028    // uninterpreted function and never constrains the model — a parse that compiles
1029    // to FOL the prover cannot use. These pin the translation.
1030
1031    #[test]
1032    fn arithmetic_offset_uses_canonical_add() {
1033        // "Tara scored 3 points higher than Bessie." → Score(tara) = add(Score(bessie), 3)
1034        let mut interner = Interner::new();
1035        let score = interner.intern("Score");
1036        let add = interner.intern("add");
1037        let tara = interner.intern("tara");
1038        let bessie = interner.intern("bessie");
1039
1040        let terms: Arena<Term> = Arena::new();
1041
1042        let score_tara = Term::Function(score, terms.alloc_slice([Term::Constant(tara)]));
1043        let score_bessie = Term::Function(score, terms.alloc_slice([Term::Constant(bessie)]));
1044        let offset = Term::Value {
1045            kind: crate::ast::logic::NumberKind::Integer(3),
1046            unit: None,
1047            dimension: None,
1048        };
1049        let rhs = Term::Function(add, terms.alloc_slice([score_bessie, offset]));
1050        let expr = LogicExpr::Identity {
1051            left: terms.alloc(score_tara),
1052            right: terms.alloc(rhs),
1053        };
1054
1055        match logic_expr_to_proof_expr(&expr, &interner) {
1056            ProofExpr::Identity(_, ProofTerm::Function(name, args)) => {
1057                assert_eq!(name, "Add", "offset function must be canonical Add; got {name}");
1058                assert!(
1059                    matches!(&args[1], ProofTerm::Constant(s) if s == "3"),
1060                    "offset constant must be the bare integer 3; got {:?}",
1061                    args[1]
1062                );
1063            }
1064            other => panic!("expected Identity with an Add rhs, got {:?}", other),
1065        }
1066    }
1067
1068    #[test]
1069    fn arithmetic_offset_uses_canonical_sub() {
1070        // "… 2 years before …" / "lower than" → ord(a) = sub(ord(b), 2)
1071        let mut interner = Interner::new();
1072        let ord = interner.intern("Ord");
1073        let sub = interner.intern("sub");
1074        let a = interner.intern("a");
1075        let b = interner.intern("b");
1076        let terms: Arena<Term> = Arena::new();
1077        let ord_a = Term::Function(ord, terms.alloc_slice([Term::Constant(a)]));
1078        let ord_b = Term::Function(ord, terms.alloc_slice([Term::Constant(b)]));
1079        let offset = Term::Value {
1080            kind: crate::ast::logic::NumberKind::Integer(2),
1081            unit: None,
1082            dimension: None,
1083        };
1084        let rhs = Term::Function(sub, terms.alloc_slice([ord_b, offset]));
1085        let expr = LogicExpr::Identity {
1086            left: terms.alloc(ord_a),
1087            right: terms.alloc(rhs),
1088        };
1089        match logic_expr_to_proof_expr(&expr, &interner) {
1090            ProofExpr::Identity(_, ProofTerm::Function(name, _)) => {
1091                assert_eq!(name, "Sub", "offset function must be canonical Sub; got {name}");
1092            }
1093            other => panic!("expected Identity with a Sub rhs, got {:?}", other),
1094        }
1095    }
1096
1097    fn convert_binary_predicate(raw_name: &str) -> String {
1098        let mut interner = Interner::new();
1099        let name = interner.intern(raw_name);
1100        let a = interner.intern("a");
1101        let b = interner.intern("b");
1102        let terms: Arena<Term> = Arena::new();
1103        let args = terms.alloc_slice([Term::Constant(a), Term::Constant(b)]);
1104        let expr = LogicExpr::Predicate { name, args, world: None };
1105        match logic_expr_to_proof_expr(&expr, &interner) {
1106            ProofExpr::Predicate { name, .. } => name,
1107            other => panic!("expected Predicate, got {:?}", other),
1108        }
1109    }
1110
1111    #[test]
1112    fn comparison_predicates_use_canonical_names() {
1113        // The parser's comparison vocabulary must reach the oracle's exact names,
1114        // and must NOT be lowercased into oblivion ("greater" ≠ "Gt").
1115        assert_eq!(convert_binary_predicate("Greater"), "Gt");
1116        assert_eq!(convert_binary_predicate("Less"), "Lt");
1117        assert_eq!(convert_binary_predicate("GreaterEqual"), "Gte");
1118        assert_eq!(convert_binary_predicate("LessEqual"), "Lte");
1119        assert_eq!(convert_binary_predicate("Equal"), "Eq");
1120        assert_eq!(convert_binary_predicate("NotEqual"), "Neq");
1121    }
1122
1123    #[test]
1124    fn ordinary_binary_predicate_is_not_remapped_to_a_comparison() {
1125        // Regression: a genuine relational predicate is unaffected by the
1126        // comparison-name mapping — it keeps the noun-normalised lowercase form
1127        // and is never hijacked into an arithmetic comparison symbol.
1128        let name = convert_binary_predicate("Loves");
1129        assert!(
1130            !["Gt", "Lt", "Gte", "Lte", "Eq", "Neq"].contains(&name.as_str()),
1131            "ordinary predicate must not become a comparison op; got {name}"
1132        );
1133        assert_eq!(
1134            name,
1135            name.to_lowercase(),
1136            "ordinary predicate keeps the noun-normalised lowercase form; got {name}"
1137        );
1138    }
1139}