Skip to main content

logicaffeine_language/
lambda.rs

1//! Lambda calculus transformations for Montague-style compositional semantics.
2//!
3//! This module provides functions for manipulating logical expressions using
4//! lambda abstraction and application. It supports:
5//!
6//! - **Quantifier raising** via type-lifting (proper names → generalized quantifiers)
7//! - **Beta reduction** for applying lambda abstractions
8//! - **Scope enumeration** to generate all possible quantifier scopings
9//! - **Intensional contexts** for opaque verbs (believes, seeks, wants)
10//! - **Event semantics** transformation (Davidson-style)
11//!
12//! # Key Functions
13//!
14//! | Function | Purpose |
15//! |----------|---------|
16//! | [`lift_proper_name`] | Lifts a proper name to a generalized quantifier |
17//! | [`lift_quantifier`] | Creates a quantifier lambda term |
18//! | [`beta_reduce`] | Performs beta reduction on applications |
19//! | [`enumerate_scopings`] | Generates all scope permutations respecting islands |
20//! | [`enumerate_intensional_readings`] | Generates de re / de dicto readings |
21//! | [`to_event_semantics`] | Converts predicate to neo-Davidsonian event form |
22//!
23//! # Scope Islands
24//!
25//! Quantifiers are assigned an `island_id` during parsing. The scoping algorithm
26//! only permutes quantifiers within the same island. This prevents:
27//! - Wh-extraction from relative clauses
28//! - Quantifier raising out of adjuncts
29//! - Other island constraint violations
30//!
31//! # Intensionality
32//!
33//! Opaque verbs create intensional contexts where substitution of co-referential
34//! terms may change truth value. The module provides:
35//! - `make_intensional`: Wraps an expression in an intensional operator
36//! - `substitute_respecting_opacity`: Blocks substitution inside intensional contexts
37
38use logicaffeine_base::Arena;
39use crate::ast::{LogicExpr, QuantifierKind, Term};
40use logicaffeine_base::{Interner, Symbol};
41use crate::lexicon;
42use crate::token::TokenType;
43
44fn clone_term<'a>(term: &Term<'a>, arena: &'a Arena<Term<'a>>) -> Term<'a> {
45    match term {
46        Term::Constant(s) => Term::Constant(*s),
47        Term::Variable(s) => Term::Variable(*s),
48        Term::Function(name, args) => {
49            let cloned_args: Vec<Term<'a>> = args.iter().map(|t| clone_term(t, arena)).collect();
50            Term::Function(*name, arena.alloc_slice(cloned_args))
51        }
52        Term::Group(members) => {
53            let cloned: Vec<Term<'a>> = members.iter().map(|t| clone_term(t, arena)).collect();
54            Term::Group(arena.alloc_slice(cloned))
55        }
56        Term::Possessed { possessor, possessed } => Term::Possessed {
57            possessor: arena.alloc(clone_term(possessor, arena)),
58            possessed: *possessed,
59        },
60        Term::Sigma(predicate) => Term::Sigma(*predicate),
61        Term::Intension(predicate) => Term::Intension(*predicate),
62        Term::Kind(kind) => Term::Kind(*kind),
63        Term::Proposition(expr) => Term::Proposition(*expr),
64        Term::Value { kind, unit, dimension } => Term::Value {
65            kind: *kind,
66            unit: *unit,
67            dimension: *dimension,
68        },
69    }
70}
71
72/// Checks if a verb creates an opaque (intensional) context.
73///
74/// Opaque verbs like "believes", "wants", "seeks" block substitution
75/// of co-referential terms. For example:
76/// - "John believes Clark Kent flies" ≠ "John believes Superman flies"
77pub fn is_opaque_verb(verb: Symbol, interner: &Interner) -> bool {
78    let verb_str = interner.resolve(verb);
79    let lower = verb_str.to_lowercase();
80    lexicon::is_opaque_verb(&lower)
81}
82
83/// Wraps an expression in an intensional operator.
84///
85/// Creates `operator[content]` for de dicto readings of intensional verbs.
86pub fn make_intensional<'a>(
87    operator: Symbol,
88    content: &'a LogicExpr<'a>,
89    arena: &'a Arena<LogicExpr<'a>>,
90) -> &'a LogicExpr<'a> {
91    arena.alloc(LogicExpr::Intensional { operator, content })
92}
93
94/// Substitutes a variable with a replacement, respecting opacity boundaries.
95///
96/// Unlike standard substitution, this function does NOT substitute inside
97/// intensional contexts, preserving the opacity of verbs like "believes".
98pub fn substitute_respecting_opacity<'a>(
99    expr: &'a LogicExpr<'a>,
100    var: Symbol,
101    replacement: &'a LogicExpr<'a>,
102    expr_arena: &'a Arena<LogicExpr<'a>>,
103    term_arena: &'a Arena<Term<'a>>,
104) -> &'a LogicExpr<'a> {
105    match expr {
106        LogicExpr::Intensional { operator, content } => {
107            expr_arena.alloc(LogicExpr::Intensional {
108                operator: *operator,
109                content: *content,
110            })
111        }
112
113        LogicExpr::Predicate { name, args, .. } => {
114            let new_args: Vec<Term<'a>> = args
115                .iter()
116                .map(|arg| substitute_term_for_opacity(arg, var, replacement, term_arena))
117                .collect();
118            expr_arena.alloc(LogicExpr::Predicate {
119                name: *name,
120                args: term_arena.alloc_slice(new_args),
121                world: None,
122            })
123        }
124
125        LogicExpr::BinaryOp { left, op, right } => expr_arena.alloc(LogicExpr::BinaryOp {
126            left: substitute_respecting_opacity(left, var, replacement, expr_arena, term_arena),
127            op: op.clone(),
128            right: substitute_respecting_opacity(right, var, replacement, expr_arena, term_arena),
129        }),
130
131        LogicExpr::UnaryOp { op, operand } => expr_arena.alloc(LogicExpr::UnaryOp {
132            op: op.clone(),
133            operand: substitute_respecting_opacity(operand, var, replacement, expr_arena, term_arena),
134        }),
135
136        LogicExpr::Quantifier { kind, variable, body, island_id } => {
137            if *variable == var {
138                expr
139            } else {
140                expr_arena.alloc(LogicExpr::Quantifier {
141                    kind: *kind,
142                    variable: *variable,
143                    body: substitute_respecting_opacity(body, var, replacement, expr_arena, term_arena),
144                    island_id: *island_id,
145                })
146            }
147        }
148
149        LogicExpr::Lambda { variable, body } => {
150            if *variable == var {
151                expr
152            } else {
153                expr_arena.alloc(LogicExpr::Lambda {
154                    variable: *variable,
155                    body: substitute_respecting_opacity(body, var, replacement, expr_arena, term_arena),
156                })
157            }
158        }
159
160        LogicExpr::App { function, argument } => expr_arena.alloc(LogicExpr::App {
161            function: substitute_respecting_opacity(function, var, replacement, expr_arena, term_arena),
162            argument: substitute_respecting_opacity(argument, var, replacement, expr_arena, term_arena),
163        }),
164
165        LogicExpr::Atom(s) => {
166            if *s == var {
167                replacement
168            } else {
169                expr
170            }
171        }
172
173        _ => expr,
174    }
175}
176
177fn substitute_term_for_opacity<'a>(
178    term: &Term<'a>,
179    var: Symbol,
180    replacement: &LogicExpr<'a>,
181    arena: &'a Arena<Term<'a>>,
182) -> Term<'a> {
183    match term {
184        Term::Constant(c) if *c == var => {
185            match replacement {
186                LogicExpr::Atom(s) => Term::Constant(*s),
187                _ => clone_term(term, arena),
188            }
189        }
190        Term::Variable(v) if *v == var => {
191            match replacement {
192                LogicExpr::Atom(s) => Term::Constant(*s),
193                _ => clone_term(term, arena),
194            }
195        }
196        _ => clone_term(term, arena),
197    }
198}
199
200/// Converts a predicate to Neo-Davidsonian event semantics.
201///
202/// Transforms `P(x, y)` into `∃e(P(e) ∧ Agent(e, x) ∧ Theme(e, y))`.
203/// This reification of events enables reasoning about event modification.
204pub fn to_event_semantics<'a>(
205    expr: &'a LogicExpr<'a>,
206    interner: &mut Interner,
207    expr_arena: &'a Arena<LogicExpr<'a>>,
208    term_arena: &'a Arena<Term<'a>>,
209) -> &'a LogicExpr<'a> {
210    match expr {
211        LogicExpr::Predicate { name, args, .. } => {
212            let e_sym = interner.intern("e");
213            let _event_var = term_arena.alloc(Term::Variable(e_sym));
214
215            let event_pred = expr_arena.alloc(LogicExpr::Predicate {
216                name: *name,
217                args: term_arena.alloc_slice([Term::Variable(e_sym)]),
218                world: None,
219            });
220
221            let mut body = event_pred;
222
223            if !args.is_empty() {
224                let agent_args = term_arena.alloc_slice([Term::Variable(e_sym), clone_term(&args[0], term_arena)]);
225                let agent_pred = expr_arena.alloc(LogicExpr::Predicate {
226                    name: interner.intern("Agent"),
227                    args: agent_args,
228                    world: None,
229                });
230                body = expr_arena.alloc(LogicExpr::BinaryOp {
231                    left: body,
232                    op: TokenType::And,
233                    right: agent_pred,
234                });
235            }
236
237            if args.len() > 1 {
238                let theme_args = term_arena.alloc_slice([Term::Variable(e_sym), clone_term(&args[1], term_arena)]);
239                let theme_pred = expr_arena.alloc(LogicExpr::Predicate {
240                    name: interner.intern("Theme"),
241                    args: theme_args,
242                    world: None,
243                });
244                body = expr_arena.alloc(LogicExpr::BinaryOp {
245                    left: body,
246                    op: TokenType::And,
247                    right: theme_pred,
248                });
249            }
250
251            if args.len() > 2 {
252                let goal_args = term_arena.alloc_slice([Term::Variable(e_sym), clone_term(&args[2], term_arena)]);
253                let goal_pred = expr_arena.alloc(LogicExpr::Predicate {
254                    name: interner.intern("Goal"),
255                    args: goal_args,
256                    world: None,
257                });
258                body = expr_arena.alloc(LogicExpr::BinaryOp {
259                    left: body,
260                    op: TokenType::And,
261                    right: goal_pred,
262                });
263            }
264
265            expr_arena.alloc(LogicExpr::Quantifier {
266                kind: QuantifierKind::Existential,
267                variable: e_sym,
268                body,
269                island_id: 0,
270            })
271        }
272        _ => expr,
273    }
274}
275
276/// Adds an adverbial modifier to an event.
277///
278/// Transforms `∃e(P(e) ∧ ...)` into `∃e(P(e) ∧ Adverb(e) ∧ ...)`.
279pub fn apply_adverb<'a>(
280    expr: &'a LogicExpr<'a>,
281    adverb: Symbol,
282    interner: &mut Interner,
283    expr_arena: &'a Arena<LogicExpr<'a>>,
284    term_arena: &'a Arena<Term<'a>>,
285) -> &'a LogicExpr<'a> {
286    let e_sym = interner.intern("e");
287    match expr {
288        LogicExpr::Quantifier { kind, variable, body, island_id } if *variable == e_sym => {
289            let adverb_str = interner.resolve(adverb);
290            let capitalized = capitalize(adverb_str);
291            let adverb_pred = expr_arena.alloc(LogicExpr::Predicate {
292                name: interner.intern(&capitalized),
293                args: term_arena.alloc_slice([Term::Variable(*variable)]),
294                world: None,
295            });
296
297            let new_body = expr_arena.alloc(LogicExpr::BinaryOp {
298                left: *body,
299                op: TokenType::And,
300                right: adverb_pred,
301            });
302
303            expr_arena.alloc(LogicExpr::Quantifier {
304                kind: *kind,
305                variable: *variable,
306                body: new_body,
307                island_id: *island_id,
308            })
309        }
310        _ => expr,
311    }
312}
313
314fn capitalize(s: &str) -> String {
315    let mut chars = s.chars();
316    match chars.next() {
317        None => String::new(),
318        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
319    }
320}
321
322fn factorial(n: usize) -> u64 {
323    (1..=n as u64).product()
324}
325
326pub struct ScopeIterator<'a> {
327    expr_arena: &'a Arena<LogicExpr<'a>>,
328    islands: Vec<Vec<ScopalElement<'a>>>,
329    core: &'a LogicExpr<'a>,
330    current_index: u64,
331    total: u64,
332    single_result: Option<&'a LogicExpr<'a>>,
333    returned_single: bool,
334}
335
336impl<'a> ScopeIterator<'a> {
337    fn nth_island_aware_permutation(&self, n: u64) -> Vec<ScopalElement<'a>> {
338        let mut result = Vec::new();
339        let mut remainder = n;
340
341        for island in &self.islands {
342            let island_perms = factorial(island.len());
343            let island_index = remainder % island_perms;
344            remainder /= island_perms;
345
346            let perm = nth_permutation_of_slice(island, island_index);
347            result.extend(perm);
348        }
349
350        result
351    }
352}
353
354fn nth_permutation_of_slice<T: Clone>(items: &[T], n: u64) -> Vec<T> {
355    let len = items.len();
356    let mut available: Vec<usize> = (0..len).collect();
357    let mut result = Vec::with_capacity(len);
358    let mut remainder = n;
359
360    for i in 0..len {
361        let divisor = factorial(len - i - 1);
362        let index = (remainder / divisor) as usize;
363        remainder %= divisor;
364        result.push(items[available.remove(index)].clone());
365    }
366    result
367}
368
369impl<'a> Iterator for ScopeIterator<'a> {
370    type Item = &'a LogicExpr<'a>;
371
372    fn next(&mut self) -> Option<Self::Item> {
373        if let Some(single) = self.single_result {
374            if self.returned_single {
375                return None;
376            }
377            self.returned_single = true;
378            return Some(single);
379        }
380
381        if self.current_index >= self.total {
382            return None;
383        }
384        let ordered = self.nth_island_aware_permutation(self.current_index);
385        self.current_index += 1;
386        Some(rebuild_with_scopal_elements(&ordered, self.core, self.expr_arena))
387    }
388
389    fn size_hint(&self) -> (usize, Option<usize>) {
390        if self.single_result.is_some() {
391            let remaining = if self.returned_single { 0 } else { 1 };
392            return (remaining, Some(remaining));
393        }
394        let remaining = (self.total - self.current_index) as usize;
395        (remaining, Some(remaining))
396    }
397}
398
399impl<'a> ExactSizeIterator for ScopeIterator<'a> {}
400
401#[derive(Clone, Debug)]
402struct QuantifierInfo<'a> {
403    kind: QuantifierKind,
404    variable: Symbol,
405    restrictor: &'a LogicExpr<'a>,
406    island_id: u32,
407}
408
409#[derive(Clone, Debug)]
410enum ScopalElement<'a> {
411    Quantifier(QuantifierInfo<'a>),
412    Negation { island_id: u32 },
413}
414
415impl<'a> ScopalElement<'a> {
416    fn island_id(&self) -> u32 {
417        match self {
418            ScopalElement::Quantifier(q) => q.island_id,
419            ScopalElement::Negation { island_id } => *island_id,
420        }
421    }
422}
423
424/// Generates all possible quantifier scopings for an expression.
425///
426/// Returns an iterator over all scope-permuted versions of the expression,
427/// respecting scope island constraints. Quantifiers with the same `island_id`
428/// can be permuted; quantifiers in different islands maintain relative order.
429///
430/// # Example
431///
432/// "Every man loves a woman" has two readings:
433/// - Surface scope: ∀x(Man(x) → ∃y(Woman(y) ∧ Love(x,y)))
434/// - Inverse scope: ∃y(Woman(y) ∧ ∀x(Man(x) → Love(x,y)))
435pub fn enumerate_scopings<'a>(
436    expr: &'a LogicExpr<'a>,
437    interner: &mut Interner,
438    expr_arena: &'a Arena<LogicExpr<'a>>,
439    _term_arena: &'a Arena<Term<'a>>,
440) -> ScopeIterator<'a> {
441    let mut elements = Vec::new();
442    let core = extract_scopal_elements(expr, &mut elements, interner, expr_arena);
443
444    if elements.is_empty() || elements.len() == 1 {
445        return ScopeIterator {
446            expr_arena,
447            islands: Vec::new(),
448            core,
449            current_index: 0,
450            total: 0,
451            single_result: Some(expr),
452            returned_single: false,
453        };
454    }
455
456    let islands = group_scopal_by_island(elements);
457    let total: u64 = islands.iter().map(|island| factorial(island.len())).product();
458
459    ScopeIterator {
460        expr_arena,
461        islands,
462        core,
463        current_index: 0,
464        total,
465        single_result: None,
466        returned_single: false,
467    }
468}
469
470fn group_by_island<'a>(quantifiers: Vec<QuantifierInfo<'a>>) -> Vec<Vec<QuantifierInfo<'a>>> {
471    use std::collections::BTreeMap;
472
473    let mut by_island: BTreeMap<u32, Vec<QuantifierInfo<'a>>> = BTreeMap::new();
474    for q in quantifiers {
475        by_island.entry(q.island_id).or_default().push(q);
476    }
477
478    by_island.into_values().collect()
479}
480
481fn group_scopal_by_island<'a>(elements: Vec<ScopalElement<'a>>) -> Vec<Vec<ScopalElement<'a>>> {
482    use std::collections::BTreeMap;
483
484    let mut by_island: BTreeMap<u32, Vec<ScopalElement<'a>>> = BTreeMap::new();
485    for elem in elements {
486        by_island.entry(elem.island_id()).or_default().push(elem);
487    }
488
489    by_island.into_values().collect()
490}
491
492/// Builds the CUMULATIVE reading (Scha 1981) for a two-cardinal transitive
493/// sentence — the reading irreducible to either nesting:
494///   "Three boys lifted five boxes." →
495///     ∃=3 x(Boy(x) ∧ ∃y(Box(y) ∧ Lift(x,y))) ∧ ∃=5 y(Box(y) ∧ ∃x(Boy(x) ∧ Lift(x,y)))
496/// i.e. exactly 3 boys each lifted some box AND exactly 5 boxes were each lifted
497/// by some boy. This is first-order over the Link lattice (no ⊕ term needed). It
498/// is produced only when there are exactly two cardinal quantifiers in the same
499/// island over a shared nucleus; otherwise returns `None`.
500pub fn cumulative_reading<'a>(
501    expr: &'a LogicExpr<'a>,
502    interner: &mut Interner,
503    expr_arena: &'a Arena<LogicExpr<'a>>,
504) -> Option<&'a LogicExpr<'a>> {
505    let mut elements = Vec::new();
506    let core = extract_scopal_elements(expr, &mut elements, interner, expr_arena);
507    if elements.len() != 2 {
508        return None;
509    }
510    let (q1, q2) = match (&elements[0], &elements[1]) {
511        (ScopalElement::Quantifier(a), ScopalElement::Quantifier(b)) => (a.clone(), b.clone()),
512        _ => return None,
513    };
514    // Both must be cardinal counts in the same island (cumulative is a relation
515    // between two counted groups, not a nesting).
516    if !matches!(q1.kind, QuantifierKind::Cardinal(_))
517        || !matches!(q2.kind, QuantifierKind::Cardinal(_))
518        || q1.island_id != q2.island_id
519    {
520        return None;
521    }
522
523    let and = |l: &'a LogicExpr<'a>, r: &'a LogicExpr<'a>| -> &'a LogicExpr<'a> {
524        expr_arena.alloc(LogicExpr::BinaryOp { left: l, op: TokenType::And, right: r })
525    };
526
527    // Conjunct 1: q1 x ( R1(x) ∧ ∃y( R2(y) ∧ core ) )
528    let exists_y = expr_arena.alloc(LogicExpr::Quantifier {
529        kind: QuantifierKind::Existential,
530        variable: q2.variable,
531        body: and(q2.restrictor, core),
532        island_id: q1.island_id,
533    });
534    let c1 = expr_arena.alloc(LogicExpr::Quantifier {
535        kind: q1.kind,
536        variable: q1.variable,
537        body: and(q1.restrictor, exists_y),
538        island_id: q1.island_id,
539    });
540
541    // Conjunct 2: q2 y ( R2(y) ∧ ∃x( R1(x) ∧ core ) )
542    let exists_x = expr_arena.alloc(LogicExpr::Quantifier {
543        kind: QuantifierKind::Existential,
544        variable: q1.variable,
545        body: and(q1.restrictor, core),
546        island_id: q2.island_id,
547    });
548    let c2 = expr_arena.alloc(LogicExpr::Quantifier {
549        kind: q2.kind,
550        variable: q2.variable,
551        body: and(q2.restrictor, exists_x),
552        island_id: q2.island_id,
553    });
554
555    Some(and(c1, c2))
556}
557
558fn extract_scopal_elements<'a>(
559    expr: &'a LogicExpr<'a>,
560    elements: &mut Vec<ScopalElement<'a>>,
561    interner: &mut Interner,
562    expr_arena: &'a Arena<LogicExpr<'a>>,
563) -> &'a LogicExpr<'a> {
564    match expr {
565        LogicExpr::Quantifier { kind, variable, body, island_id } => {
566            if let LogicExpr::BinaryOp { left, op, right } = body {
567                if matches!(op, TokenType::If | TokenType::Implies | TokenType::And) {
568                    // Check if right side has a negation at the top level
569                    if let LogicExpr::UnaryOp { op: TokenType::Not, operand } = right {
570                        // Pattern: ∀x(R(x) → ¬P(x)) or ∃x(R(x) ∧ ¬P(x))
571                        // Extract both quantifier and negation
572                        elements.push(ScopalElement::Quantifier(QuantifierInfo {
573                            kind: *kind,
574                            variable: *variable,
575                            restrictor: *left,
576                            island_id: *island_id,
577                        }));
578                        elements.push(ScopalElement::Negation { island_id: *island_id });
579                        return extract_scopal_elements(operand, elements, interner, expr_arena);
580                    }
581                    // No negation in right side, just extract quantifier
582                    elements.push(ScopalElement::Quantifier(QuantifierInfo {
583                        kind: *kind,
584                        variable: *variable,
585                        restrictor: *left,
586                        island_id: *island_id,
587                    }));
588                    return extract_scopal_elements(right, elements, interner, expr_arena);
589                }
590            }
591            // No binary op body, use a true restrictor
592            elements.push(ScopalElement::Quantifier(QuantifierInfo {
593                kind: *kind,
594                variable: *variable,
595                restrictor: expr_arena.alloc(LogicExpr::Atom(interner.intern("T"))),
596                island_id: *island_id,
597            }));
598            extract_scopal_elements(body, elements, interner, expr_arena)
599        }
600        LogicExpr::UnaryOp { op: TokenType::Not, operand } => {
601            // Standalone negation (not inside a quantifier body)
602            elements.push(ScopalElement::Negation { island_id: 0 });
603            extract_scopal_elements(operand, elements, interner, expr_arena)
604        }
605        _ => expr,
606    }
607}
608
609fn rebuild_with_scopal_elements<'a>(
610    elements: &[ScopalElement<'a>],
611    core: &'a LogicExpr<'a>,
612    arena: &'a Arena<LogicExpr<'a>>,
613) -> &'a LogicExpr<'a> {
614    let mut result = core;
615
616    for elem in elements.iter().rev() {
617        match elem {
618            ScopalElement::Quantifier(q) => {
619                let connective = match q.kind {
620                    QuantifierKind::Universal => TokenType::Implies,
621                    _ => TokenType::And,
622                };
623
624                let body = arena.alloc(LogicExpr::BinaryOp {
625                    left: q.restrictor,
626                    op: connective,
627                    right: result,
628                });
629
630                result = arena.alloc(LogicExpr::Quantifier {
631                    kind: q.kind,
632                    variable: q.variable,
633                    body,
634                    island_id: q.island_id,
635                });
636            }
637            ScopalElement::Negation { .. } => {
638                result = arena.alloc(LogicExpr::UnaryOp {
639                    op: TokenType::Not,
640                    operand: result,
641                });
642            }
643        }
644    }
645
646    result
647}
648
649fn extract_quantifiers<'a>(
650    expr: &'a LogicExpr<'a>,
651    quantifiers: &mut Vec<QuantifierInfo<'a>>,
652    interner: &mut Interner,
653    expr_arena: &'a Arena<LogicExpr<'a>>,
654) -> &'a LogicExpr<'a> {
655    match expr {
656        LogicExpr::Quantifier { kind, variable, body, island_id } => {
657            if let LogicExpr::BinaryOp { left, op, right } = body {
658                if matches!(op, TokenType::If | TokenType::Implies | TokenType::And) {
659                    quantifiers.push(QuantifierInfo {
660                        kind: *kind,
661                        variable: *variable,
662                        restrictor: *left,
663                        island_id: *island_id,
664                    });
665                    return extract_quantifiers(right, quantifiers, interner, expr_arena);
666                }
667            }
668            quantifiers.push(QuantifierInfo {
669                kind: *kind,
670                variable: *variable,
671                restrictor: expr_arena.alloc(LogicExpr::Atom(interner.intern("T"))),
672                island_id: *island_id,
673            });
674            extract_quantifiers(body, quantifiers, interner, expr_arena)
675        }
676        _ => expr,
677    }
678}
679
680fn rebuild_with_scope_order<'a>(
681    quantifiers: &[QuantifierInfo<'a>],
682    core: &'a LogicExpr<'a>,
683    arena: &'a Arena<LogicExpr<'a>>,
684) -> &'a LogicExpr<'a> {
685    let mut result = core;
686
687    for q in quantifiers.iter().rev() {
688        let connective = match q.kind {
689            QuantifierKind::Universal => TokenType::Implies,
690            _ => TokenType::And,
691        };
692
693        let body = arena.alloc(LogicExpr::BinaryOp {
694            left: q.restrictor,
695            op: connective,
696            right: result,
697        });
698
699        result = arena.alloc(LogicExpr::Quantifier {
700            kind: q.kind,
701            variable: q.variable,
702            body,
703            island_id: q.island_id,
704        });
705    }
706
707    result
708}
709
710pub fn lift_proper_name<'a>(
711    name: Symbol,
712    interner: &mut Interner,
713    arena: &'a Arena<LogicExpr<'a>>,
714) -> &'a LogicExpr<'a> {
715    let p_sym = interner.intern("P");
716    let inner_app = arena.alloc(LogicExpr::App {
717        function: arena.alloc(LogicExpr::Atom(p_sym)),
718        argument: arena.alloc(LogicExpr::Atom(name)),
719    });
720    arena.alloc(LogicExpr::Lambda {
721        variable: p_sym,
722        body: inner_app,
723    })
724}
725
726pub fn lift_quantifier<'a>(
727    kind: QuantifierKind,
728    restrictor: Symbol,
729    interner: &mut Interner,
730    expr_arena: &'a Arena<LogicExpr<'a>>,
731    term_arena: &'a Arena<Term<'a>>,
732) -> &'a LogicExpr<'a> {
733    let x_sym = interner.intern("x");
734    let q_sym = interner.intern("Q");
735
736    let restrictor_pred = expr_arena.alloc(LogicExpr::Predicate {
737        name: restrictor,
738        args: term_arena.alloc_slice([Term::Variable(x_sym)]),
739        world: None,
740    });
741
742    let q_of_x = expr_arena.alloc(LogicExpr::App {
743        function: expr_arena.alloc(LogicExpr::Atom(q_sym)),
744        argument: expr_arena.alloc(LogicExpr::Atom(x_sym)),
745    });
746
747    let connective = match kind {
748        QuantifierKind::Universal => TokenType::Implies,
749        _ => TokenType::And,
750    };
751
752    let body = expr_arena.alloc(LogicExpr::BinaryOp {
753        left: restrictor_pred,
754        op: connective,
755        right: q_of_x,
756    });
757
758    let quantifier = expr_arena.alloc(LogicExpr::Quantifier {
759        kind,
760        variable: x_sym,
761        body,
762        island_id: 0,
763    });
764
765    expr_arena.alloc(LogicExpr::Lambda {
766        variable: q_sym,
767        body: quantifier,
768    })
769}
770
771/// Performs beta reduction on lambda applications.
772///
773/// Reduces `(λx.body)(arg)` to `body[x := arg]` by substituting
774/// the argument for the bound variable in the body.
775pub fn beta_reduce<'a>(
776    expr: &'a LogicExpr<'a>,
777    expr_arena: &'a Arena<LogicExpr<'a>>,
778    term_arena: &'a Arena<Term<'a>>,
779) -> &'a LogicExpr<'a> {
780    match expr {
781        LogicExpr::App { function, argument } => {
782            if let LogicExpr::Lambda { variable, body } = function {
783                substitute(body, *variable, argument, expr_arena, term_arena)
784            } else {
785                expr_arena.alloc(LogicExpr::App {
786                    function: beta_reduce(function, expr_arena, term_arena),
787                    argument: beta_reduce(argument, expr_arena, term_arena),
788                })
789            }
790        }
791        LogicExpr::Lambda { variable, body } => expr_arena.alloc(LogicExpr::Lambda {
792            variable: *variable,
793            body: beta_reduce(body, expr_arena, term_arena),
794        }),
795        _ => expr,
796    }
797}
798
799fn substitute<'a>(
800    expr: &'a LogicExpr<'a>,
801    var: Symbol,
802    replacement: &'a LogicExpr<'a>,
803    expr_arena: &'a Arena<LogicExpr<'a>>,
804    term_arena: &'a Arena<Term<'a>>,
805) -> &'a LogicExpr<'a> {
806    match expr {
807        LogicExpr::Predicate { name, args, .. } => {
808            let new_args: Vec<Term<'a>> = args
809                .iter()
810                .map(|arg| substitute_term(arg, var, replacement, term_arena))
811                .collect();
812            expr_arena.alloc(LogicExpr::Predicate {
813                name: *name,
814                args: term_arena.alloc_slice(new_args),
815                world: None,
816            })
817        }
818
819        LogicExpr::Lambda { variable, body } => {
820            if *variable == var {
821                expr
822            } else {
823                expr_arena.alloc(LogicExpr::Lambda {
824                    variable: *variable,
825                    body: substitute(body, var, replacement, expr_arena, term_arena),
826                })
827            }
828        }
829
830        LogicExpr::App { function, argument } => expr_arena.alloc(LogicExpr::App {
831            function: substitute(function, var, replacement, expr_arena, term_arena),
832            argument: substitute(argument, var, replacement, expr_arena, term_arena),
833        }),
834
835        LogicExpr::BinaryOp { left, op, right } => expr_arena.alloc(LogicExpr::BinaryOp {
836            left: substitute(left, var, replacement, expr_arena, term_arena),
837            op: op.clone(),
838            right: substitute(right, var, replacement, expr_arena, term_arena),
839        }),
840
841        LogicExpr::UnaryOp { op, operand } => expr_arena.alloc(LogicExpr::UnaryOp {
842            op: op.clone(),
843            operand: substitute(operand, var, replacement, expr_arena, term_arena),
844        }),
845
846        LogicExpr::Quantifier { kind, variable, body, island_id } => {
847            if *variable == var {
848                expr
849            } else {
850                expr_arena.alloc(LogicExpr::Quantifier {
851                    kind: *kind,
852                    variable: *variable,
853                    body: substitute(body, var, replacement, expr_arena, term_arena),
854                    island_id: *island_id,
855                })
856            }
857        }
858
859        LogicExpr::Atom(s) => {
860            if *s == var {
861                replacement
862            } else {
863                expr
864            }
865        }
866
867        _ => expr,
868    }
869}
870
871fn substitute_term<'a>(
872    term: &Term<'a>,
873    var: Symbol,
874    replacement: &LogicExpr<'a>,
875    term_arena: &'a Arena<Term<'a>>,
876) -> Term<'a> {
877    match term {
878        Term::Variable(v) if *v == var => {
879            match replacement {
880                LogicExpr::Atom(s) => Term::Constant(*s),
881                LogicExpr::Predicate { name, .. } => Term::Constant(*name),
882                _ => clone_term(term, term_arena),
883            }
884        }
885        _ => clone_term(term, term_arena),
886    }
887}
888
889// ═══════════════════════════════════════════════════════════════════
890// Intensional Reading Generation (De Re / De Dicto)
891// ═══════════════════════════════════════════════════════════════════
892
893#[derive(Debug)]
894struct IntensionalContext {
895    verb: Symbol,
896    quantifier_var: Symbol,
897    restrictor: Symbol,
898}
899
900fn find_opaque_verb_context<'a>(
901    expr: &'a LogicExpr<'a>,
902    interner: &Interner,
903) -> Option<IntensionalContext> {
904    match expr {
905        LogicExpr::Quantifier { kind: QuantifierKind::Existential, variable, body, .. } => {
906            if let LogicExpr::BinaryOp { left, op: TokenType::And, right } = body {
907                if let LogicExpr::Predicate { name: restrictor, args, .. } = left {
908                    if args.len() == 1 {
909                        if let Term::Variable(v) = &args[0] {
910                            if *v == *variable {
911                                if let Some(verb) = find_opaque_verb_in_scope(right, *variable, interner) {
912                                    return Some(IntensionalContext {
913                                        verb,
914                                        quantifier_var: *variable,
915                                        restrictor: *restrictor,
916                                    });
917                                }
918                            }
919                        }
920                    }
921                }
922            }
923            None
924        }
925        _ => None,
926    }
927}
928
929fn find_opaque_verb_in_scope<'a>(
930    expr: &'a LogicExpr<'a>,
931    theme_var: Symbol,
932    interner: &Interner,
933) -> Option<Symbol> {
934    match expr {
935        LogicExpr::Quantifier { body, .. } => find_opaque_verb_in_scope(body, theme_var, interner),
936        LogicExpr::BinaryOp { left, right, .. } => {
937            find_opaque_verb_in_scope(left, theme_var, interner)
938                .or_else(|| find_opaque_verb_in_scope(right, theme_var, interner))
939        }
940        LogicExpr::NeoEvent(data) => {
941            if is_opaque_verb(data.verb, interner) {
942                for (role, term) in data.roles.iter() {
943                    if matches!(role, crate::ast::ThematicRole::Theme) {
944                        if let Term::Variable(v) = term {
945                            if *v == theme_var {
946                                return Some(data.verb);
947                            }
948                        }
949                    }
950                }
951            }
952            None
953        }
954        LogicExpr::Predicate { name, args, .. } => {
955            if is_opaque_verb(*name, interner) && args.len() >= 2 {
956                if let Term::Variable(v) = &args[1] {
957                    if *v == theme_var {
958                        return Some(*name);
959                    }
960                }
961            }
962            None
963        }
964        _ => None,
965    }
966}
967
968fn build_de_dicto_reading<'a>(
969    expr: &'a LogicExpr<'a>,
970    ctx: &IntensionalContext,
971    expr_arena: &'a Arena<LogicExpr<'a>>,
972    term_arena: &'a Arena<Term<'a>>,
973    role_arena: &'a Arena<(crate::ast::ThematicRole, Term<'a>)>,
974) -> &'a LogicExpr<'a> {
975    match expr {
976        LogicExpr::Quantifier { kind: QuantifierKind::Existential, variable, body, .. }
977            if *variable == ctx.quantifier_var =>
978        {
979            if let LogicExpr::BinaryOp { right, .. } = body {
980                replace_theme_with_intension(right, ctx, expr_arena, term_arena, role_arena)
981            } else {
982                expr
983            }
984        }
985        _ => expr,
986    }
987}
988
989fn replace_theme_with_intension<'a>(
990    expr: &'a LogicExpr<'a>,
991    ctx: &IntensionalContext,
992    expr_arena: &'a Arena<LogicExpr<'a>>,
993    term_arena: &'a Arena<Term<'a>>,
994    role_arena: &'a Arena<(crate::ast::ThematicRole, Term<'a>)>,
995) -> &'a LogicExpr<'a> {
996    match expr {
997        LogicExpr::Quantifier { kind, variable, body, island_id } => {
998            let new_body = replace_theme_with_intension(body, ctx, expr_arena, term_arena, role_arena);
999            expr_arena.alloc(LogicExpr::Quantifier {
1000                kind: *kind,
1001                variable: *variable,
1002                body: new_body,
1003                island_id: *island_id,
1004            })
1005        }
1006        LogicExpr::BinaryOp { left, op, right } => {
1007            let new_left = replace_theme_with_intension(left, ctx, expr_arena, term_arena, role_arena);
1008            let new_right = replace_theme_with_intension(right, ctx, expr_arena, term_arena, role_arena);
1009            expr_arena.alloc(LogicExpr::BinaryOp {
1010                left: new_left,
1011                op: op.clone(),
1012                right: new_right,
1013            })
1014        }
1015        LogicExpr::NeoEvent(data) => {
1016            let new_roles: Vec<_> = data.roles.iter().map(|(role, term)| {
1017                if matches!(role, crate::ast::ThematicRole::Theme) {
1018                    if let Term::Variable(v) = term {
1019                        if *v == ctx.quantifier_var {
1020                            return (*role, Term::Intension(ctx.restrictor));
1021                        }
1022                    }
1023                }
1024                (*role, clone_term(term, term_arena))
1025            }).collect();
1026
1027            expr_arena.alloc(LogicExpr::NeoEvent(Box::new(crate::ast::NeoEventData {
1028                event_var: data.event_var,
1029                verb: data.verb,
1030                roles: role_arena.alloc_slice(new_roles),
1031                modifiers: data.modifiers,
1032                suppress_existential: false,
1033                world: None,
1034            })))
1035        }
1036        LogicExpr::Predicate { name, args, .. } => {
1037            let new_args: Vec<_> = args.iter().map(|arg| {
1038                if let Term::Variable(v) = arg {
1039                    if *v == ctx.quantifier_var {
1040                        return Term::Intension(ctx.restrictor);
1041                    }
1042                }
1043                clone_term(arg, term_arena)
1044            }).collect();
1045
1046            expr_arena.alloc(LogicExpr::Predicate {
1047                name: *name,
1048                args: term_arena.alloc_slice(new_args),
1049                world: None,
1050            })
1051        }
1052        _ => expr,
1053    }
1054}
1055
1056pub fn enumerate_intensional_readings<'a>(
1057    expr: &'a LogicExpr<'a>,
1058    interner: &mut Interner,
1059    expr_arena: &'a Arena<LogicExpr<'a>>,
1060    term_arena: &'a Arena<Term<'a>>,
1061    role_arena: &'a Arena<(crate::ast::ThematicRole, Term<'a>)>,
1062) -> Vec<&'a LogicExpr<'a>> {
1063    // Check if expression already has intensional terms (de dicto from parser)
1064    if let Some(de_re) = build_de_re_from_de_dicto(expr, interner, expr_arena, term_arena, role_arena) {
1065        // Return both: de re first (existential), de dicto second (intension)
1066        return vec![de_re, expr];
1067    }
1068
1069    // Clausal attitude complement with an embedded existential
1070    // ("believes ⟨∃x(Spy(x) ∧ Exist(x))⟩"): the de re reading raises the
1071    // existential over the attitude — ∃x(Spy(x) ∧ Believe(s, ⟨Exist(x)⟩)).
1072    if let Some(de_re) = raise_existential_from_proposition(expr, expr_arena, term_arena) {
1073        return vec![expr, de_re];
1074    }
1075
1076    // Original logic: check for de re that can be converted to de dicto
1077    if let Some(ctx) = find_opaque_verb_context(expr, interner) {
1078        let de_dicto = build_de_dicto_reading(expr, &ctx, expr_arena, term_arena, role_arena);
1079        vec![expr, de_dicto]
1080    } else {
1081        vec![expr]
1082    }
1083}
1084
1085/// De re quantifier raising out of a clausal attitude complement:
1086/// `V(s, ⟨∃x(R(x) ∧ P(x))⟩)` → `∃x(R(x) ∧ V(s, ⟨P(x)⟩))`. A `Temporal`
1087/// wrapper on the attitude stays on the attitude.
1088fn raise_existential_from_proposition<'a>(
1089    expr: &'a LogicExpr<'a>,
1090    expr_arena: &'a Arena<LogicExpr<'a>>,
1091    term_arena: &'a Arena<Term<'a>>,
1092) -> Option<&'a LogicExpr<'a>> {
1093    use crate::token::TokenType;
1094
1095    let (temporal_op, attitude) = match expr {
1096        LogicExpr::Temporal { operator, body } => (Some(*operator), *body),
1097        other => (None, other),
1098    };
1099
1100    let LogicExpr::Predicate { name, args, world } = attitude else {
1101        return None;
1102    };
1103    if args.len() != 2 {
1104        return None;
1105    }
1106    let Term::Proposition(LogicExpr::Quantifier {
1107        kind: crate::ast::QuantifierKind::Existential,
1108        variable,
1109        body,
1110        island_id,
1111    }) = &args[1]
1112    else {
1113        return None;
1114    };
1115    let LogicExpr::BinaryOp {
1116        left: restriction,
1117        op: TokenType::And,
1118        right: inner,
1119    } = body
1120    else {
1121        return None;
1122    };
1123
1124    let raised_attitude = expr_arena.alloc(LogicExpr::Predicate {
1125        name: *name,
1126        args: term_arena.alloc_slice([args[0].clone(), Term::Proposition(inner)]),
1127        world: world.clone(),
1128    });
1129    let raised_attitude = match temporal_op {
1130        Some(operator) => expr_arena.alloc(LogicExpr::Temporal {
1131            operator,
1132            body: raised_attitude,
1133        }),
1134        None => raised_attitude,
1135    };
1136    let raised_body = expr_arena.alloc(LogicExpr::BinaryOp {
1137        left: restriction,
1138        op: TokenType::And,
1139        right: raised_attitude,
1140    });
1141    Some(expr_arena.alloc(LogicExpr::Quantifier {
1142        kind: crate::ast::QuantifierKind::Existential,
1143        variable: *variable,
1144        body: raised_body,
1145        island_id: *island_id,
1146    }))
1147}
1148
1149fn build_de_re_from_de_dicto<'a>(
1150    expr: &'a LogicExpr<'a>,
1151    interner: &mut Interner,
1152    expr_arena: &'a Arena<LogicExpr<'a>>,
1153    term_arena: &'a Arena<Term<'a>>,
1154    role_arena: &'a Arena<(crate::ast::ThematicRole, Term<'a>)>,
1155) -> Option<&'a LogicExpr<'a>> {
1156    // Find Term::Intension in NeoEvent themes and expand to existential
1157    match expr {
1158        LogicExpr::NeoEvent(data) => {
1159            // Check if any role has an Intension term
1160            for (role, term) in data.roles.iter() {
1161                if matches!(role, crate::ast::ThematicRole::Theme) {
1162                    if let Term::Intension(noun) = term {
1163                        // Build de re: ∃x(Noun(x) ∧ Event[Theme=x])
1164                        let var = interner.intern("x");
1165
1166                        // Build noun predicate: Noun(x)
1167                        let noun_pred = expr_arena.alloc(LogicExpr::Predicate {
1168                            name: *noun,
1169                            args: term_arena.alloc_slice([Term::Variable(var)]),
1170                            world: None,
1171                        });
1172
1173                        // Build new roles with variable instead of intension
1174                        let new_roles: Vec<_> = data.roles.iter().map(|(r, t)| {
1175                            if matches!(r, crate::ast::ThematicRole::Theme) {
1176                                (*r, Term::Variable(var))
1177                            } else {
1178                                (*r, t.clone())
1179                            }
1180                        }).collect();
1181
1182                        let new_event = expr_arena.alloc(LogicExpr::NeoEvent(Box::new(crate::ast::NeoEventData {
1183                            event_var: data.event_var,
1184                            verb: data.verb,
1185                            roles: role_arena.alloc_slice(new_roles),
1186                            modifiers: data.modifiers,
1187                            suppress_existential: false,
1188                            world: None,
1189                        })));
1190
1191                        // Build: ∃x(Noun(x) ∧ Event)
1192                        let body = expr_arena.alloc(LogicExpr::BinaryOp {
1193                            left: noun_pred,
1194                            op: crate::token::TokenType::And,
1195                            right: new_event,
1196                        });
1197
1198                        return Some(expr_arena.alloc(LogicExpr::Quantifier {
1199                            kind: crate::ast::QuantifierKind::Existential,
1200                            variable: var,
1201                            body,
1202                            island_id: 0,
1203                        }));
1204                    }
1205                }
1206            }
1207            None
1208        }
1209        _ => None,
1210    }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    use super::*;
1216    use crate::ast::{LogicExpr, Term};
1217    use logicaffeine_base::Interner;
1218    use crate::registry::SymbolRegistry;
1219    use crate::OutputFormat;
1220
1221    #[test]
1222    fn test_lambda_formatting_unicode() {
1223        let mut interner = Interner::new();
1224        let expr_arena: Arena<LogicExpr> = Arena::new();
1225        let term_arena: Arena<Term> = Arena::new();
1226
1227        let x = interner.intern("x");
1228        let sleep = interner.intern("Sleep");
1229
1230        let body = expr_arena.alloc(LogicExpr::Predicate {
1231            name: sleep,
1232            args: term_arena.alloc_slice([Term::Variable(x)]),
1233            world: None,
1234        });
1235        let lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body });
1236
1237        let mut registry = SymbolRegistry::new();
1238        let output = lambda.transpile(&mut registry, &interner, OutputFormat::Unicode);
1239        assert!(output.contains("λx"), "Unicode should use λ: {}", output);
1240    }
1241
1242    #[test]
1243    fn test_lambda_formatting_latex() {
1244        let mut interner = Interner::new();
1245        let expr_arena: Arena<LogicExpr> = Arena::new();
1246        let term_arena: Arena<Term> = Arena::new();
1247
1248        let x = interner.intern("x");
1249        let sleep = interner.intern("Sleep");
1250
1251        let body = expr_arena.alloc(LogicExpr::Predicate {
1252            name: sleep,
1253            args: term_arena.alloc_slice([Term::Variable(x)]),
1254            world: None,
1255        });
1256        let lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body });
1257
1258        let mut registry = SymbolRegistry::new();
1259        let output = lambda.transpile(&mut registry, &interner, OutputFormat::LaTeX);
1260        assert!(output.contains("\\lambda"), "LaTeX should use \\lambda: {}", output);
1261    }
1262
1263    #[test]
1264    fn test_application_formatting() {
1265        let mut interner = Interner::new();
1266        let expr_arena: Arena<LogicExpr> = Arena::new();
1267
1268        let p = interner.intern("P");
1269        let j = interner.intern("j");
1270
1271        let func = expr_arena.alloc(LogicExpr::Atom(p));
1272        let arg = expr_arena.alloc(LogicExpr::Atom(j));
1273        let app = expr_arena.alloc(LogicExpr::App { function: func, argument: arg });
1274
1275        let mut registry = SymbolRegistry::new();
1276        let output = app.transpile(&mut registry, &interner, OutputFormat::Unicode);
1277        assert!(output.contains("(") && output.contains(")"), "App should have parens: {}", output);
1278    }
1279
1280    #[test]
1281    fn test_nested_lambda() {
1282        let mut interner = Interner::new();
1283        let expr_arena: Arena<LogicExpr> = Arena::new();
1284
1285        let x = interner.intern("x");
1286        let y = interner.intern("y");
1287
1288        let inner_body = expr_arena.alloc(LogicExpr::Atom(x));
1289        let inner_lambda = expr_arena.alloc(LogicExpr::Lambda { variable: y, body: inner_body });
1290        let outer_lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body: inner_lambda });
1291
1292        let mut registry = SymbolRegistry::new();
1293        let output = outer_lambda.transpile(&mut registry, &interner, OutputFormat::Unicode);
1294        assert!(output.contains("λx") && output.contains("λy"), "Nested lambdas: {}", output);
1295    }
1296
1297    #[test]
1298    fn test_lambda_app_helper_functions() {
1299        let mut interner = Interner::new();
1300        let expr_arena: Arena<LogicExpr> = Arena::new();
1301        let _term_arena: Arena<Term> = Arena::new();
1302
1303        let x = interner.intern("x");
1304        let p = interner.intern("P");
1305
1306        let body = expr_arena.alloc(LogicExpr::Atom(x));
1307        let lambda = LogicExpr::lambda(x, body, &expr_arena);
1308
1309        let arg = expr_arena.alloc(LogicExpr::Atom(p));
1310        let app = LogicExpr::app(lambda, arg, &expr_arena);
1311
1312        assert!(matches!(app, LogicExpr::App { .. }));
1313    }
1314
1315    #[test]
1316    fn lift_proper_name_returns_lambda() {
1317        let mut interner = Interner::new();
1318        let arena: Arena<LogicExpr> = Arena::new();
1319
1320        let john = interner.intern("John");
1321        let lifted = lift_proper_name(john, &mut interner, &arena);
1322
1323        assert!(matches!(lifted, LogicExpr::Lambda { .. }), "Should return Lambda");
1324    }
1325
1326    #[test]
1327    fn lift_proper_name_applies_predicate() {
1328        let mut interner = Interner::new();
1329        let arena: Arena<LogicExpr> = Arena::new();
1330
1331        let john = interner.intern("John");
1332        let lifted = lift_proper_name(john, &mut interner, &arena);
1333
1334        if let LogicExpr::Lambda { body, .. } = lifted {
1335            assert!(matches!(body, LogicExpr::App { .. }), "Body should be App");
1336        } else {
1337            panic!("Expected Lambda");
1338        }
1339    }
1340
1341    #[test]
1342    fn lift_quantifier_universal_returns_lambda() {
1343        let mut interner = Interner::new();
1344        let expr_arena: Arena<LogicExpr> = Arena::new();
1345        let term_arena: Arena<Term> = Arena::new();
1346
1347        let woman = interner.intern("woman");
1348        let lifted = lift_quantifier(QuantifierKind::Universal, woman, &mut interner, &expr_arena, &term_arena);
1349
1350        assert!(matches!(lifted, LogicExpr::Lambda { .. }), "Should return Lambda");
1351    }
1352
1353    #[test]
1354    fn lift_quantifier_universal_structure() {
1355        let mut interner = Interner::new();
1356        let expr_arena: Arena<LogicExpr> = Arena::new();
1357        let term_arena: Arena<Term> = Arena::new();
1358
1359        let woman = interner.intern("woman");
1360        let lifted = lift_quantifier(QuantifierKind::Universal, woman, &mut interner, &expr_arena, &term_arena);
1361
1362        if let LogicExpr::Lambda { body, .. } = lifted {
1363            assert!(
1364                matches!(body, LogicExpr::Quantifier { kind: QuantifierKind::Universal, .. }),
1365                "Body should contain ∀, got {:?}",
1366                body
1367            );
1368        } else {
1369            panic!("Expected Lambda, got {:?}", lifted);
1370        }
1371    }
1372
1373    #[test]
1374    fn lift_quantifier_existential_returns_lambda() {
1375        let mut interner = Interner::new();
1376        let expr_arena: Arena<LogicExpr> = Arena::new();
1377        let term_arena: Arena<Term> = Arena::new();
1378
1379        let man = interner.intern("man");
1380        let lifted = lift_quantifier(QuantifierKind::Existential, man, &mut interner, &expr_arena, &term_arena);
1381
1382        assert!(matches!(lifted, LogicExpr::Lambda { .. }), "Should return Lambda");
1383    }
1384
1385    #[test]
1386    fn lift_quantifier_existential_structure() {
1387        let mut interner = Interner::new();
1388        let expr_arena: Arena<LogicExpr> = Arena::new();
1389        let term_arena: Arena<Term> = Arena::new();
1390
1391        let man = interner.intern("man");
1392        let lifted = lift_quantifier(QuantifierKind::Existential, man, &mut interner, &expr_arena, &term_arena);
1393
1394        if let LogicExpr::Lambda { body, .. } = lifted {
1395            assert!(
1396                matches!(body, LogicExpr::Quantifier { kind: QuantifierKind::Existential, .. }),
1397                "Body should contain ∃, got {:?}",
1398                body
1399            );
1400        } else {
1401            panic!("Expected Lambda, got {:?}", lifted);
1402        }
1403    }
1404
1405    #[test]
1406    fn beta_reduce_simple_predicate() {
1407        let mut interner = Interner::new();
1408        let expr_arena: Arena<LogicExpr> = Arena::new();
1409        let term_arena: Arena<Term> = Arena::new();
1410
1411        let x = interner.intern("x");
1412        let john = interner.intern("John");
1413        let run = interner.intern("Run");
1414
1415        let body = expr_arena.alloc(LogicExpr::Predicate {
1416            name: run,
1417            args: term_arena.alloc_slice([Term::Variable(x)]),
1418            world: None,
1419        });
1420        let lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body });
1421        let arg = expr_arena.alloc(LogicExpr::Atom(john));
1422        let app = expr_arena.alloc(LogicExpr::App { function: lambda, argument: arg });
1423
1424        let reduced = beta_reduce(app, &expr_arena, &term_arena);
1425
1426        let mut registry = SymbolRegistry::new();
1427        let output = reduced.transpile(&mut registry, &interner, OutputFormat::Unicode);
1428        assert!(output.contains("R(J)") || output.contains("Run(John)"), "Should substitute: {}", output);
1429    }
1430
1431    #[test]
1432    fn beta_reduce_with_constant() {
1433        let mut interner = Interner::new();
1434        let expr_arena: Arena<LogicExpr> = Arena::new();
1435        let term_arena: Arena<Term> = Arena::new();
1436
1437        let x = interner.intern("x");
1438        let c = interner.intern("c");
1439
1440        let body = expr_arena.alloc(LogicExpr::Atom(c));
1441        let lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body });
1442        let arg = expr_arena.alloc(LogicExpr::Atom(interner.intern("anything")));
1443        let app = expr_arena.alloc(LogicExpr::App { function: lambda, argument: arg });
1444
1445        let reduced = beta_reduce(app, &expr_arena, &term_arena);
1446        assert!(matches!(reduced, LogicExpr::Atom(s) if *s == c), "Constant should remain");
1447    }
1448
1449    #[test]
1450    fn beta_reduce_nested_lambda() {
1451        let mut interner = Interner::new();
1452        let expr_arena: Arena<LogicExpr> = Arena::new();
1453        let term_arena: Arena<Term> = Arena::new();
1454
1455        let x = interner.intern("x");
1456        let y = interner.intern("y");
1457
1458        let inner_body = expr_arena.alloc(LogicExpr::Atom(x));
1459        let inner_lambda = expr_arena.alloc(LogicExpr::Lambda { variable: y, body: inner_body });
1460        let outer_lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body: inner_lambda });
1461
1462        let reduced = beta_reduce(outer_lambda, &expr_arena, &term_arena);
1463        assert!(matches!(reduced, LogicExpr::Lambda { .. }), "Should still be lambda");
1464    }
1465
1466    #[test]
1467    fn beta_reduce_non_application_unchanged() {
1468        let mut interner = Interner::new();
1469        let expr_arena: Arena<LogicExpr> = Arena::new();
1470        let term_arena: Arena<Term> = Arena::new();
1471
1472        let p = interner.intern("P");
1473        let atom = expr_arena.alloc(LogicExpr::Atom(p));
1474
1475        let reduced = beta_reduce(atom, &expr_arena, &term_arena);
1476        assert!(matches!(reduced, LogicExpr::Atom(s) if *s == p), "Atom unchanged");
1477    }
1478
1479    #[test]
1480    fn beta_reduce_preserves_unbound_variables() {
1481        let mut interner = Interner::new();
1482        let expr_arena: Arena<LogicExpr> = Arena::new();
1483        let term_arena: Arena<Term> = Arena::new();
1484
1485        let x = interner.intern("x");
1486        let y = interner.intern("y");
1487        let john = interner.intern("John");
1488        let loves = interner.intern("Loves");
1489
1490        let body = expr_arena.alloc(LogicExpr::Predicate {
1491            name: loves,
1492            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]),
1493            world: None,
1494        });
1495        let lambda = expr_arena.alloc(LogicExpr::Lambda { variable: x, body });
1496        let arg = expr_arena.alloc(LogicExpr::Atom(john));
1497        let app = expr_arena.alloc(LogicExpr::App { function: lambda, argument: arg });
1498
1499        let reduced = beta_reduce(app, &expr_arena, &term_arena);
1500
1501        let mut registry = SymbolRegistry::new();
1502        let output = reduced.transpile(&mut registry, &interner, OutputFormat::Unicode);
1503        assert!(output.contains("y"), "y should remain unbound: {}", output);
1504    }
1505
1506    #[test]
1507    fn enumerate_scopings_single_quantifier() {
1508        let mut interner = Interner::new();
1509        let expr_arena: Arena<LogicExpr> = Arena::new();
1510        let term_arena: Arena<Term> = Arena::new();
1511
1512        let x = interner.intern("x");
1513        let dog = interner.intern("Dog");
1514        let bark = interner.intern("Bark");
1515
1516        let left = expr_arena.alloc(LogicExpr::Predicate {
1517            name: dog,
1518            args: term_arena.alloc_slice([Term::Variable(x)]),
1519            world: None,
1520        });
1521        let right = expr_arena.alloc(LogicExpr::Predicate {
1522            name: bark,
1523            args: term_arena.alloc_slice([Term::Variable(x)]),
1524            world: None,
1525        });
1526        let body = expr_arena.alloc(LogicExpr::BinaryOp {
1527            left,
1528            op: TokenType::Implies,
1529            right,
1530        });
1531        let expr = expr_arena.alloc(LogicExpr::Quantifier {
1532            kind: QuantifierKind::Universal,
1533            variable: x,
1534            body,
1535            island_id: 0,
1536        });
1537
1538        let scopings = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena);
1539        assert_eq!(scopings.len(), 1, "Single quantifier should have 1 reading");
1540    }
1541
1542    #[test]
1543    fn enumerate_scopings_no_quantifier() {
1544        let mut interner = Interner::new();
1545        let expr_arena: Arena<LogicExpr> = Arena::new();
1546        let term_arena: Arena<Term> = Arena::new();
1547
1548        let run = interner.intern("Run");
1549        let john = interner.intern("John");
1550
1551        let expr = expr_arena.alloc(LogicExpr::Predicate {
1552            name: run,
1553            args: term_arena.alloc_slice([Term::Constant(john)]),
1554            world: None,
1555        });
1556
1557        let scopings = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena);
1558        assert_eq!(scopings.len(), 1, "No quantifiers should have 1 reading");
1559    }
1560
1561    #[test]
1562    fn is_opaque_verb_believes() {
1563        let mut interner = Interner::new();
1564        let believes = interner.intern("believes");
1565        let believes_cap = interner.intern("Believes");
1566        assert!(is_opaque_verb(believes, &interner), "believes should be opaque");
1567        assert!(is_opaque_verb(believes_cap, &interner), "Believes should be opaque");
1568    }
1569
1570    #[test]
1571    fn is_opaque_verb_seeks() {
1572        let mut interner = Interner::new();
1573        let seeks = interner.intern("seeks");
1574        let wants = interner.intern("wants");
1575        assert!(is_opaque_verb(seeks, &interner), "seeks should be opaque");
1576        assert!(is_opaque_verb(wants, &interner), "wants should be opaque");
1577    }
1578
1579    #[test]
1580    fn is_opaque_verb_normal_verbs() {
1581        let mut interner = Interner::new();
1582        let runs = interner.intern("runs");
1583        let loves = interner.intern("loves");
1584        assert!(!is_opaque_verb(runs, &interner), "runs should NOT be opaque");
1585        assert!(!is_opaque_verb(loves, &interner), "loves should NOT be opaque");
1586    }
1587
1588    #[test]
1589    fn make_intensional_creates_wrapper() {
1590        let mut interner = Interner::new();
1591        let expr_arena: Arena<LogicExpr> = Arena::new();
1592        let term_arena: Arena<Term> = Arena::new();
1593
1594        let weak = interner.intern("Weak");
1595        let clark = interner.intern("Clark");
1596        let believes = interner.intern("believes");
1597
1598        let content = expr_arena.alloc(LogicExpr::Predicate {
1599            name: weak,
1600            args: term_arena.alloc_slice([Term::Constant(clark)]),
1601            world: None,
1602        });
1603
1604        let intensional = make_intensional(believes, content, &expr_arena);
1605
1606        assert!(
1607            matches!(intensional, LogicExpr::Intensional { operator, .. } if *operator == believes),
1608            "Should create Intensional wrapper, got {:?}",
1609            intensional
1610        );
1611    }
1612
1613    #[test]
1614    fn intensional_transpiles_with_brackets() {
1615        let mut interner = Interner::new();
1616        let expr_arena: Arena<LogicExpr> = Arena::new();
1617        let term_arena: Arena<Term> = Arena::new();
1618
1619        let weak = interner.intern("Weak");
1620        let clark = interner.intern("Clark");
1621        let believes = interner.intern("Believes");
1622
1623        let content = expr_arena.alloc(LogicExpr::Predicate {
1624            name: weak,
1625            args: term_arena.alloc_slice([Term::Constant(clark)]),
1626            world: None,
1627        });
1628
1629        let intensional = expr_arena.alloc(LogicExpr::Intensional {
1630            operator: believes,
1631            content,
1632        });
1633
1634        let mut registry = SymbolRegistry::new();
1635        let output = intensional.transpile(&mut registry, &interner, OutputFormat::Unicode);
1636
1637        assert!(
1638            output.contains("[") && output.contains("]"),
1639            "Intensional should use brackets: got {}",
1640            output
1641        );
1642    }
1643
1644    #[test]
1645    fn substitute_respecting_opacity_blocks_inside_intensional() {
1646        let mut interner = Interner::new();
1647        let expr_arena: Arena<LogicExpr> = Arena::new();
1648        let term_arena: Arena<Term> = Arena::new();
1649
1650        let weak = interner.intern("Weak");
1651        let clark = interner.intern("Clark");
1652        let believes = interner.intern("Believes");
1653        let superman = interner.intern("Superman");
1654
1655        let inner = expr_arena.alloc(LogicExpr::Predicate {
1656            name: weak,
1657            args: term_arena.alloc_slice([Term::Constant(clark)]),
1658            world: None,
1659        });
1660        let expr = expr_arena.alloc(LogicExpr::Intensional {
1661            operator: believes,
1662            content: inner,
1663        });
1664
1665        let replacement = expr_arena.alloc(LogicExpr::Atom(superman));
1666        let result = substitute_respecting_opacity(expr, clark, replacement, &expr_arena, &term_arena);
1667
1668        let mut registry = SymbolRegistry::new();
1669        let output = result.transpile(&mut registry, &interner, OutputFormat::Unicode);
1670
1671        assert!(
1672            output.contains("C") && !output.contains("S"),
1673            "Should NOT substitute inside intensional context: got {}",
1674            output
1675        );
1676    }
1677
1678    #[test]
1679    fn substitute_respecting_opacity_allows_outside() {
1680        let mut interner = Interner::new();
1681        let expr_arena: Arena<LogicExpr> = Arena::new();
1682        let term_arena: Arena<Term> = Arena::new();
1683
1684        let weak = interner.intern("Weak");
1685        let clark = interner.intern("Clark");
1686        let superman = interner.intern("Superman");
1687
1688        let expr = expr_arena.alloc(LogicExpr::Predicate {
1689            name: weak,
1690            args: term_arena.alloc_slice([Term::Constant(clark)]),
1691            world: None,
1692        });
1693
1694        let replacement = expr_arena.alloc(LogicExpr::Atom(superman));
1695        let result = substitute_respecting_opacity(expr, clark, replacement, &expr_arena, &term_arena);
1696
1697        let mut registry = SymbolRegistry::new();
1698        let output = result.transpile(&mut registry, &interner, OutputFormat::Unicode);
1699
1700        assert!(
1701            output.contains("S"),
1702            "Should substitute outside intensional context: got {}",
1703            output
1704        );
1705    }
1706
1707    #[test]
1708    fn factorial_basic() {
1709        assert_eq!(factorial(0), 1);
1710        assert_eq!(factorial(1), 1);
1711        assert_eq!(factorial(2), 2);
1712        assert_eq!(factorial(3), 6);
1713        assert_eq!(factorial(4), 24);
1714        assert_eq!(factorial(5), 120);
1715    }
1716
1717    #[test]
1718    fn scope_iterator_two_quantifiers_yields_two() {
1719        let mut interner = Interner::new();
1720        let expr_arena: Arena<LogicExpr> = Arena::new();
1721        let term_arena: Arena<Term> = Arena::new();
1722
1723        let x = interner.intern("x");
1724        let y = interner.intern("y");
1725        let man = interner.intern("Man");
1726        let woman = interner.intern("Woman");
1727        let loves = interner.intern("Loves");
1728
1729        let man_x = expr_arena.alloc(LogicExpr::Predicate {
1730            name: man,
1731            args: term_arena.alloc_slice([Term::Variable(x)]),
1732            world: None,
1733        });
1734        let woman_y = expr_arena.alloc(LogicExpr::Predicate {
1735            name: woman,
1736            args: term_arena.alloc_slice([Term::Variable(y)]),
1737            world: None,
1738        });
1739        let loves_xy = expr_arena.alloc(LogicExpr::Predicate {
1740            name: loves,
1741            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]),
1742            world: None,
1743        });
1744
1745        let inner = expr_arena.alloc(LogicExpr::BinaryOp {
1746            left: woman_y,
1747            op: TokenType::And,
1748            right: loves_xy,
1749        });
1750        let inner_q = expr_arena.alloc(LogicExpr::Quantifier {
1751            kind: QuantifierKind::Existential,
1752            variable: y,
1753            body: inner,
1754            island_id: 0,
1755        });
1756
1757        let outer = expr_arena.alloc(LogicExpr::BinaryOp {
1758            left: man_x,
1759            op: TokenType::Implies,
1760            right: inner_q,
1761        });
1762        let expr = expr_arena.alloc(LogicExpr::Quantifier {
1763            kind: QuantifierKind::Universal,
1764            variable: x,
1765            body: outer,
1766            island_id: 0,
1767        });
1768
1769        let scopings: Vec<_> = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena).collect();
1770        assert_eq!(scopings.len(), 2, "Two quantifiers should have 2! = 2 readings");
1771    }
1772
1773    #[test]
1774    fn scope_iterator_three_quantifiers_yields_six() {
1775        let mut interner = Interner::new();
1776        let expr_arena: Arena<LogicExpr> = Arena::new();
1777        let term_arena: Arena<Term> = Arena::new();
1778
1779        let x = interner.intern("x");
1780        let y = interner.intern("y");
1781        let z = interner.intern("z");
1782        let man = interner.intern("Man");
1783        let woman = interner.intern("Woman");
1784        let book = interner.intern("Book");
1785        let gives = interner.intern("Gives");
1786
1787        let man_x = expr_arena.alloc(LogicExpr::Predicate {
1788            name: man,
1789            args: term_arena.alloc_slice([Term::Variable(x)]),
1790            world: None,
1791        });
1792        let woman_y = expr_arena.alloc(LogicExpr::Predicate {
1793            name: woman,
1794            args: term_arena.alloc_slice([Term::Variable(y)]),
1795            world: None,
1796        });
1797        let book_z = expr_arena.alloc(LogicExpr::Predicate {
1798            name: book,
1799            args: term_arena.alloc_slice([Term::Variable(z)]),
1800            world: None,
1801        });
1802        let gives_xyz = expr_arena.alloc(LogicExpr::Predicate {
1803            name: gives,
1804            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y), Term::Variable(z)]),
1805            world: None,
1806        });
1807
1808        let inner_z = expr_arena.alloc(LogicExpr::BinaryOp {
1809            left: book_z,
1810            op: TokenType::And,
1811            right: gives_xyz,
1812        });
1813        let q_z = expr_arena.alloc(LogicExpr::Quantifier {
1814            kind: QuantifierKind::Existential,
1815            variable: z,
1816            body: inner_z,
1817            island_id: 0,
1818        });
1819
1820        let inner_y = expr_arena.alloc(LogicExpr::BinaryOp {
1821            left: woman_y,
1822            op: TokenType::And,
1823            right: q_z,
1824        });
1825        let q_y = expr_arena.alloc(LogicExpr::Quantifier {
1826            kind: QuantifierKind::Existential,
1827            variable: y,
1828            body: inner_y,
1829            island_id: 0,
1830        });
1831
1832        let outer = expr_arena.alloc(LogicExpr::BinaryOp {
1833            left: man_x,
1834            op: TokenType::Implies,
1835            right: q_y,
1836        });
1837        let expr = expr_arena.alloc(LogicExpr::Quantifier {
1838            kind: QuantifierKind::Universal,
1839            variable: x,
1840            body: outer,
1841            island_id: 0,
1842        });
1843
1844        let scopings: Vec<_> = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena).collect();
1845        assert_eq!(scopings.len(), 6, "Three quantifiers should have 3! = 6 readings");
1846    }
1847
1848    #[test]
1849    fn scope_iterator_no_duplicates() {
1850        use std::collections::HashSet;
1851
1852        let mut interner = Interner::new();
1853        let expr_arena: Arena<LogicExpr> = Arena::new();
1854        let term_arena: Arena<Term> = Arena::new();
1855
1856        let x = interner.intern("x");
1857        let y = interner.intern("y");
1858        let man = interner.intern("Man");
1859        let woman = interner.intern("Woman");
1860        let loves = interner.intern("Loves");
1861
1862        let man_x = expr_arena.alloc(LogicExpr::Predicate {
1863            name: man,
1864            args: term_arena.alloc_slice([Term::Variable(x)]),
1865            world: None,
1866        });
1867        let woman_y = expr_arena.alloc(LogicExpr::Predicate {
1868            name: woman,
1869            args: term_arena.alloc_slice([Term::Variable(y)]),
1870            world: None,
1871        });
1872        let loves_xy = expr_arena.alloc(LogicExpr::Predicate {
1873            name: loves,
1874            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]),
1875            world: None,
1876        });
1877
1878        let inner = expr_arena.alloc(LogicExpr::BinaryOp {
1879            left: woman_y,
1880            op: TokenType::And,
1881            right: loves_xy,
1882        });
1883        let inner_q = expr_arena.alloc(LogicExpr::Quantifier {
1884            kind: QuantifierKind::Existential,
1885            variable: y,
1886            body: inner,
1887            island_id: 0,
1888        });
1889
1890        let outer = expr_arena.alloc(LogicExpr::BinaryOp {
1891            left: man_x,
1892            op: TokenType::Implies,
1893            right: inner_q,
1894        });
1895        let expr = expr_arena.alloc(LogicExpr::Quantifier {
1896            kind: QuantifierKind::Universal,
1897            variable: x,
1898            body: outer,
1899            island_id: 0,
1900        });
1901
1902        let mut registry = SymbolRegistry::new();
1903        let outputs: HashSet<String> = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena)
1904            .map(|e| e.transpile(&mut registry, &interner, OutputFormat::Unicode))
1905            .collect();
1906
1907        assert_eq!(outputs.len(), 2, "All scopings should be unique");
1908    }
1909
1910    #[test]
1911    fn scope_iterator_exact_size() {
1912        let mut interner = Interner::new();
1913        let expr_arena: Arena<LogicExpr> = Arena::new();
1914        let term_arena: Arena<Term> = Arena::new();
1915
1916        let x = interner.intern("x");
1917        let y = interner.intern("y");
1918        let man = interner.intern("Man");
1919        let woman = interner.intern("Woman");
1920        let loves = interner.intern("Loves");
1921
1922        let man_x = expr_arena.alloc(LogicExpr::Predicate {
1923            name: man,
1924            args: term_arena.alloc_slice([Term::Variable(x)]),
1925            world: None,
1926        });
1927        let woman_y = expr_arena.alloc(LogicExpr::Predicate {
1928            name: woman,
1929            args: term_arena.alloc_slice([Term::Variable(y)]),
1930            world: None,
1931        });
1932        let loves_xy = expr_arena.alloc(LogicExpr::Predicate {
1933            name: loves,
1934            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]),
1935            world: None,
1936        });
1937
1938        let inner = expr_arena.alloc(LogicExpr::BinaryOp {
1939            left: woman_y,
1940            op: TokenType::And,
1941            right: loves_xy,
1942        });
1943        let inner_q = expr_arena.alloc(LogicExpr::Quantifier {
1944            kind: QuantifierKind::Existential,
1945            variable: y,
1946            body: inner,
1947            island_id: 0,
1948        });
1949
1950        let outer = expr_arena.alloc(LogicExpr::BinaryOp {
1951            left: man_x,
1952            op: TokenType::Implies,
1953            right: inner_q,
1954        });
1955        let expr = expr_arena.alloc(LogicExpr::Quantifier {
1956            kind: QuantifierKind::Universal,
1957            variable: x,
1958            body: outer,
1959            island_id: 0,
1960        });
1961
1962        let mut iter = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena);
1963        assert_eq!(iter.len(), 2);
1964        iter.next();
1965        assert_eq!(iter.len(), 1);
1966        iter.next();
1967        assert_eq!(iter.len(), 0);
1968    }
1969
1970    #[test]
1971    fn island_constraints_reduce_permutations() {
1972        let mut interner = Interner::new();
1973        let expr_arena: Arena<LogicExpr> = Arena::new();
1974        let term_arena: Arena<Term> = Arena::new();
1975
1976        let x = interner.intern("x");
1977        let y = interner.intern("y");
1978        let man = interner.intern("Man");
1979        let woman = interner.intern("Woman");
1980        let loves = interner.intern("Loves");
1981
1982        let man_x = expr_arena.alloc(LogicExpr::Predicate {
1983            name: man,
1984            args: term_arena.alloc_slice([Term::Variable(x)]),
1985            world: None,
1986        });
1987        let woman_y = expr_arena.alloc(LogicExpr::Predicate {
1988            name: woman,
1989            args: term_arena.alloc_slice([Term::Variable(y)]),
1990            world: None,
1991        });
1992        let loves_xy = expr_arena.alloc(LogicExpr::Predicate {
1993            name: loves,
1994            args: term_arena.alloc_slice([Term::Variable(x), Term::Variable(y)]),
1995            world: None,
1996        });
1997
1998        let inner = expr_arena.alloc(LogicExpr::BinaryOp {
1999            left: woman_y,
2000            op: TokenType::And,
2001            right: loves_xy,
2002        });
2003        let inner_q = expr_arena.alloc(LogicExpr::Quantifier {
2004            kind: QuantifierKind::Existential,
2005            variable: y,
2006            body: inner,
2007            island_id: 1,
2008        });
2009
2010        let outer = expr_arena.alloc(LogicExpr::BinaryOp {
2011            left: man_x,
2012            op: TokenType::Implies,
2013            right: inner_q,
2014        });
2015        let expr = expr_arena.alloc(LogicExpr::Quantifier {
2016            kind: QuantifierKind::Universal,
2017            variable: x,
2018            body: outer,
2019            island_id: 0,
2020        });
2021
2022        let scopings = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena);
2023        assert_eq!(
2024            scopings.len(),
2025            1,
2026            "Two quantifiers in different islands: 1! × 1! = 1 reading (no cross-island scoping)"
2027        );
2028    }
2029
2030    #[test]
2031    fn multiple_quantifiers_per_island() {
2032        let mut interner = Interner::new();
2033        let expr_arena: Arena<LogicExpr> = Arena::new();
2034        let term_arena: Arena<Term> = Arena::new();
2035
2036        let x = interner.intern("x");
2037        let y = interner.intern("y");
2038        let z = interner.intern("z");
2039        let w = interner.intern("w");
2040        let pred = interner.intern("P");
2041
2042        let core = expr_arena.alloc(LogicExpr::Predicate {
2043            name: pred,
2044            args: term_arena.alloc_slice([
2045                Term::Variable(x),
2046                Term::Variable(y),
2047                Term::Variable(z),
2048                Term::Variable(w),
2049            ]),
2050            world: None,
2051        });
2052
2053        let true_sym = interner.intern("T");
2054        let t = expr_arena.alloc(LogicExpr::Atom(true_sym));
2055
2056        let q_w = expr_arena.alloc(LogicExpr::Quantifier {
2057            kind: QuantifierKind::Existential,
2058            variable: w,
2059            body: expr_arena.alloc(LogicExpr::BinaryOp { left: t, op: TokenType::And, right: core }),
2060            island_id: 1,
2061        });
2062        let q_z = expr_arena.alloc(LogicExpr::Quantifier {
2063            kind: QuantifierKind::Existential,
2064            variable: z,
2065            body: expr_arena.alloc(LogicExpr::BinaryOp { left: t, op: TokenType::And, right: q_w }),
2066            island_id: 1,
2067        });
2068        let q_y = expr_arena.alloc(LogicExpr::Quantifier {
2069            kind: QuantifierKind::Existential,
2070            variable: y,
2071            body: expr_arena.alloc(LogicExpr::BinaryOp { left: t, op: TokenType::And, right: q_z }),
2072            island_id: 0,
2073        });
2074        let expr = expr_arena.alloc(LogicExpr::Quantifier {
2075            kind: QuantifierKind::Universal,
2076            variable: x,
2077            body: expr_arena.alloc(LogicExpr::BinaryOp { left: t, op: TokenType::Implies, right: q_y }),
2078            island_id: 0,
2079        });
2080
2081        let scopings = enumerate_scopings(expr, &mut interner, &expr_arena, &term_arena);
2082        assert_eq!(
2083            scopings.len(),
2084            4,
2085            "4 quantifiers split 2+2 across islands: 2! × 2! = 4 (not 4! = 24)"
2086        );
2087    }
2088}