Skip to main content

logicaffeine_language/
drs.rs

1//! Discourse Representation Structure (DRS) for anaphora resolution and scope tracking.
2//!
3//! This module implements a simplified DRS following the theory developed by Hans Kamp
4//! and Uwe Reyle. The DRS tracks referents (discourse entities) across sentences and
5//! handles pronoun resolution, donkey anaphora, and quantifier scope.
6//!
7//! # Core Concepts
8//!
9//! - **Box**: A scope container holding referents and conditions. Boxes nest to form
10//!   scope islands (conditionals, quantifiers, negation).
11//! - **Referent**: A discourse entity with a variable, noun class, gender, and number.
12//!   Introduced by indefinites, proper names, or quantifiers.
13//! - **Accessibility**: Whether a referent can be accessed from a given scope.
14//!   Negation and disjunction block accessibility outward.
15//!
16//! # Key Types
17//!
18//! | Type | Purpose |
19//! |------|---------|
20//! | [`Drs`] | The box hierarchy tracking referents and their scopes |
21//! | [`WorldState`] | Unified discourse state persisting across sentences |
22//! | [`BoxType`] | Classification of scope containers (Main, Negation, Modal, etc.) |
23//! | [`Referent`] | A discourse entity with gender, number, and source information |
24//! | [`ScopeError`] | Error when pronoun resolution fails due to scope constraints |
25//!
26//! # Accessibility Rules
27//!
28//! A pronoun in box B can access referent R in box A if:
29//! 1. A is B (same box)
30//! 2. A is an ancestor of B (parent chain)
31//! 3. A is a conditional antecedent and B is the consequent of the same conditional
32//! 4. A is a universal restrictor and B is the universal scope
33//!
34//! Referents in **negation** or **disjunction** boxes are NEVER accessible from outside.
35//!
36//! # Example
37//!
38//! "If a farmer owns a donkey, he beats it."
39//! - "a farmer" introduces referent x in conditional antecedent box
40//! - "a donkey" introduces referent y in conditional antecedent box
41//! - "he" resolves to x (accessible from consequent)
42//! - "it" resolves to y (accessible from consequent)
43//! - Both receive universal quantification due to conditional DRS signature
44
45use logicaffeine_base::Symbol;
46use std::fmt;
47
48// Re-export lexicon types for DRS usage
49pub use logicaffeine_lexicon::types::{Gender, Number, Case};
50
51// ============================================
52// CORE DISCOURSE TYPES (moved from context.rs)
53// ============================================
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum TimeRelation {
57    Precedes,
58    Equals,
59}
60
61#[derive(Debug, Clone)]
62pub struct TimeConstraint {
63    pub left: String,
64    pub relation: TimeRelation,
65    pub right: String,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69pub enum OwnershipState {
70    #[default]
71    Owned,
72    Moved,
73    Borrowed,
74}
75
76// ============================================
77// SCOPE ERROR TYPES
78// ============================================
79
80/// Error when pronoun resolution fails due to scope constraints
81#[derive(Debug, Clone, PartialEq)]
82pub enum ScopeError {
83    /// Referent exists but is trapped in an inaccessible scope
84    InaccessibleReferent {
85        gender: Gender,
86        blocking_scope: BoxType,
87        reason: String,
88    },
89    /// No matching referent found at all
90    NoMatchingReferent {
91        gender: Gender,
92        number: Number,
93    },
94}
95
96impl fmt::Display for ScopeError {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            ScopeError::InaccessibleReferent { gender, blocking_scope, reason } => {
100                write!(f, "Cannot resolve {:?} pronoun: referent is trapped in {:?} scope. {}",
101                    gender, blocking_scope, reason)
102            }
103            ScopeError::NoMatchingReferent { gender, number } => {
104                write!(f, "Cannot resolve {:?} {:?} pronoun: no matching referent in accessible scope",
105                    gender, number)
106            }
107        }
108    }
109}
110
111impl std::error::Error for ScopeError {}
112
113// ============================================
114// TELESCOPE SUPPORT
115// ============================================
116
117/// Path segment for navigating to insertion point during AST restructuring
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum ScopePath {
120    /// Enter body of ∀ or ∃ quantifier
121    QuantifierBody,
122    /// Enter consequent of → implication
123    ImplicationRight,
124    /// Enter right side of ∧ conjunction
125    ConjunctionRight,
126}
127
128/// A referent that may be accessed via telescoping across sentence boundaries
129#[derive(Debug, Clone)]
130pub struct TelescopeCandidate {
131    pub variable: Symbol,
132    pub noun_class: Symbol,
133    pub gender: Gender,
134    /// The box index where this referent was introduced
135    pub origin_box: usize,
136    /// Path to navigate AST for scope extension
137    pub scope_path: Vec<ScopePath>,
138    /// Whether this referent was introduced in a modal scope
139    pub in_modal_scope: bool,
140}
141
142// ============================================
143// MODAL SUBORDINATION SUPPORT
144// ============================================
145
146/// Modal context for tracking hypothetical worlds across sentences.
147/// Enables modal subordination: "A wolf might walk in. It would eat you."
148#[derive(Debug, Clone)]
149pub struct ModalContext {
150    /// Whether we're currently inside a modal scope
151    pub active: bool,
152    /// The modal flavor (epistemic vs root)
153    pub is_epistemic: bool,
154    /// Force value (0.0 = impossibility, 1.0 = necessity)
155    pub force: f32,
156}
157
158// ============================================
159// WORLD STATE (Unified Discourse State)
160// ============================================
161
162/// The unified discourse state that persists across sentences.
163#[derive(Debug, Clone)]
164pub struct WorldState {
165    /// The global DRS (box hierarchy for scope tracking)
166    pub drs: Drs,
167    /// Event variable counter (e1, e2, e3...)
168    event_counter: usize,
169    /// Event history for temporal ordering
170    event_history: Vec<String>,
171    /// Reference time counter (r1, r2, r3...)
172    reference_time_counter: usize,
173    /// Current reference time
174    current_reference_time: Option<String>,
175    /// Temporal constraints between events
176    time_constraints: Vec<TimeConstraint>,
177    /// Telescope candidates from previous sentence
178    telescope_candidates: Vec<TelescopeCandidate>,
179    /// Whether we're in discourse mode (processing multi-sentence discourse)
180    /// When true, unresolved pronouns should error instead of deictic fallback
181    discourse_mode: bool,
182    /// Current modal context (if any) for tracking modal scope
183    current_modal_context: Option<ModalContext>,
184    /// Modal context from previous sentence for subordination
185    prior_modal_context: Option<ModalContext>,
186}
187
188impl WorldState {
189    pub fn new() -> Self {
190        Self {
191            drs: Drs::new(),
192            event_counter: 0,
193            event_history: Vec::new(),
194            reference_time_counter: 0,
195            current_reference_time: None,
196            time_constraints: Vec::new(),
197            telescope_candidates: Vec::new(),
198            discourse_mode: false,
199            current_modal_context: None,
200            prior_modal_context: None,
201        }
202    }
203
204    /// Generate next event variable (e1, e2, e3...)
205    pub fn next_event_var(&mut self) -> String {
206        self.event_counter += 1;
207        let var = format!("e{}", self.event_counter);
208        self.event_history.push(var.clone());
209        var
210    }
211
212    /// Get event history for temporal ordering
213    pub fn event_history(&self) -> &[String] {
214        &self.event_history
215    }
216
217    /// Generate next reference time (r1, r2, r3...)
218    pub fn next_reference_time(&mut self) -> String {
219        self.reference_time_counter += 1;
220        let var = format!("r{}", self.reference_time_counter);
221        self.current_reference_time = Some(var.clone());
222        var
223    }
224
225    /// Get current reference time
226    pub fn current_reference_time(&self) -> String {
227        self.current_reference_time.clone().unwrap_or_else(|| "S".to_string())
228    }
229
230    /// Add a temporal constraint
231    pub fn add_time_constraint(&mut self, left: String, relation: TimeRelation, right: String) {
232        self.time_constraints.push(TimeConstraint { left, relation, right });
233    }
234
235    /// Get all time constraints
236    pub fn time_constraints(&self) -> &[TimeConstraint] {
237        &self.time_constraints
238    }
239
240    /// Clear time constraints (for sentence boundary reset if needed)
241    pub fn clear_time_constraints(&mut self) {
242        self.time_constraints.clear();
243        self.reference_time_counter = 0;
244        self.current_reference_time = None;
245    }
246
247    /// Mark a sentence boundary - collect telescope candidates
248    pub fn end_sentence(&mut self) {
249        // Collect referents that can telescope from current DRS state
250        let mut candidates = self.drs.get_telescope_candidates();
251
252        // MODAL BARRIER: If this sentence had a modal, mark ALL its referents as modal-sourced.
253        // This handles "A wolf might enter" where the wolf is introduced BEFORE we see "might".
254        // The wolf should be marked as hypothetical even though it's in the main DRS box.
255        if self.current_modal_context.is_some() {
256            for candidate in &mut candidates {
257                candidate.in_modal_scope = true;
258            }
259        }
260
261        self.telescope_candidates = candidates;
262        // Capture modal context for subordination in next sentence
263        self.prior_modal_context = self.current_modal_context.take();
264        // Mark that we're now in discourse mode (multi-sentence context)
265        self.discourse_mode = true;
266    }
267
268    /// Check if we're in discourse mode (multi-sentence context)
269    /// In discourse mode, unresolved pronouns should error instead of deictic fallback
270    pub fn in_discourse_mode(&self) -> bool {
271        self.discourse_mode
272    }
273
274    /// Get telescope candidates from previous sentence
275    pub fn telescope_candidates(&self) -> &[TelescopeCandidate] {
276        &self.telescope_candidates
277    }
278
279    /// Try to resolve a pronoun via telescoping
280    pub fn resolve_via_telescope(&mut self, gender: Gender) -> Option<TelescopeCandidate> {
281        // MODAL BARRIER: Check if we can access hypothetical entities
282        // Reality (indicative) cannot see into imagination (modal scope)
283        // Only modal subordination (e.g., "would" following "might") can access modal candidates
284        let can_access_modal = self.in_modal_context();
285
286        #[cfg(debug_assertions)]
287
288        // Apply same Gender Accommodation rules as resolve_pronoun:
289        // - Exact match (Male=Male, Female=Female, etc)
290        // - Unknown referent matches any pronoun (Gender Accommodation)
291        // - Unknown pronoun matches any referent
292        for candidate in &self.telescope_candidates {
293            // MODAL BARRIER CHECK: Skip hypothetical entities when in reality mode
294            if candidate.in_modal_scope && !can_access_modal {
295                // Wolf in imagination cannot be referenced from reality
296                #[cfg(debug_assertions)]
297                continue;
298            }
299
300            let gender_match = candidate.gender == gender
301                || candidate.gender == Gender::Unknown  // Gender Accommodation
302                || gender == Gender::Unknown;
303
304            if gender_match {
305                return Some(candidate.clone());
306            }
307        }
308
309        None
310    }
311
312    /// Set ownership state for a referent by noun class
313    pub fn set_ownership(&mut self, noun_class: Symbol, state: OwnershipState) {
314        self.drs.set_ownership(noun_class, state);
315    }
316
317    /// Get ownership state for a referent by noun class
318    pub fn get_ownership(&self, noun_class: Symbol) -> Option<OwnershipState> {
319        self.drs.get_ownership(noun_class)
320    }
321
322    /// Set ownership state for a referent by variable name
323    pub fn set_ownership_by_var(&mut self, var: Symbol, state: OwnershipState) {
324        self.drs.set_ownership_by_var(var, state);
325    }
326
327    /// Get ownership state for a referent by variable name
328    pub fn get_ownership_by_var(&self, var: Symbol) -> Option<OwnershipState> {
329        self.drs.get_ownership_by_var(var)
330    }
331
332    // ============================================
333    // MODAL SUBORDINATION METHODS
334    // ============================================
335
336    /// Enter a modal context (e.g., "might", "would", "could")
337    pub fn enter_modal_context(&mut self, is_epistemic: bool, force: f32) {
338        self.current_modal_context = Some(ModalContext {
339            active: true,
340            is_epistemic,
341            force,
342        });
343        // Also enter a modal box in the DRS
344        self.drs.enter_box(BoxType::ModalScope);
345    }
346
347    /// Exit the current modal context
348    pub fn exit_modal_context(&mut self) {
349        self.current_modal_context = None;
350        self.drs.exit_box();
351    }
352
353    /// Check if we're currently in a modal context
354    pub fn in_modal_context(&self) -> bool {
355        self.current_modal_context.is_some()
356    }
357
358    /// Check if there's a prior modal context for subordination
359    pub fn has_prior_modal_context(&self) -> bool {
360        self.prior_modal_context.is_some()
361    }
362
363    /// Check if current modal can subordinate to prior context
364    /// "would" can continue a "might" world
365    pub fn can_subordinate(&self) -> bool {
366        self.prior_modal_context.is_some()
367    }
368
369    /// Clear the world state (reset for new discourse)
370    pub fn clear(&mut self) {
371        self.drs.clear();
372        self.event_counter = 0;
373        self.event_history.clear();
374        self.reference_time_counter = 0;
375        self.current_reference_time = None;
376        self.time_constraints.clear();
377        self.telescope_candidates.clear();
378        self.discourse_mode = false;
379        self.current_modal_context = None;
380        self.prior_modal_context = None;
381    }
382}
383
384impl Default for WorldState {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390// ============================================
391// REFERENT SOURCE
392// ============================================
393
394#[derive(Debug, Clone, Copy, PartialEq, Eq)]
395pub enum ReferentSource {
396    /// Indefinite in main clause - gets existential force
397    MainClause,
398    /// Proper name - no quantifier (constant)
399    ProperName,
400    /// Indefinite in conditional antecedent - gets universal force (DRS signature)
401    ConditionalAntecedent,
402    /// Indefinite in universal restrictor (relative clause) - gets universal force
403    UniversalRestrictor,
404    /// Inside negation scope - inaccessible outward
405    NegationScope,
406    /// Inside disjunction - inaccessible outward
407    Disjunct,
408    /// Inside modal scope - accessible via modal subordination
409    ModalScope,
410}
411
412impl ReferentSource {
413    pub fn gets_universal_force(&self) -> bool {
414        matches!(
415            self,
416            ReferentSource::ConditionalAntecedent | ReferentSource::UniversalRestrictor
417        )
418    }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum BoxType {
423    /// Top-level discourse box
424    Main,
425    /// Antecedent of conditional ("if" clause)
426    ConditionalAntecedent,
427    /// Consequent of conditional ("then" clause)
428    ConditionalConsequent,
429    /// Scope of negation
430    NegationScope,
431    /// Restrictor of universal quantifier (relative clause in "every X who...")
432    UniversalRestrictor,
433    /// Nuclear scope of universal quantifier
434    UniversalScope,
435    /// Branch of disjunction
436    Disjunct,
437    /// Scope of modal operator (might, would, could, etc.)
438    /// Allows modal subordination: pronouns can access referents via telescoping
439    ModalScope,
440}
441
442impl BoxType {
443    pub fn to_referent_source(&self) -> ReferentSource {
444        match self {
445            BoxType::Main => ReferentSource::MainClause,
446            BoxType::ConditionalAntecedent => ReferentSource::ConditionalAntecedent,
447            BoxType::ConditionalConsequent => ReferentSource::MainClause,
448            BoxType::NegationScope => ReferentSource::NegationScope,
449            BoxType::UniversalRestrictor => ReferentSource::UniversalRestrictor,
450            BoxType::UniversalScope => ReferentSource::MainClause,
451            BoxType::Disjunct => ReferentSource::Disjunct,
452            BoxType::ModalScope => ReferentSource::ModalScope,
453        }
454    }
455
456    /// Can referents in this box be accessed via telescoping across sentence boundaries?
457    /// Universal quantifiers, conditionals, and modals CAN telescope.
458    /// Negation and disjunction CANNOT telescope.
459    pub fn can_telescope(&self) -> bool {
460        matches!(
461            self,
462            BoxType::Main
463            | BoxType::UniversalScope
464            | BoxType::UniversalRestrictor
465            | BoxType::ConditionalConsequent
466            | BoxType::ConditionalAntecedent
467            | BoxType::ModalScope  // Modal subordination allows cross-sentence access
468        )
469        // NegationScope and Disjunct return false implicitly
470    }
471
472    /// Does this box type block accessibility from outside?
473    pub fn blocks_accessibility(&self) -> bool {
474        matches!(self, BoxType::NegationScope | BoxType::Disjunct)
475    }
476}
477
478/// Two modifier sets match when they contain exactly the same symbols
479/// (order-independent). A description with NO modifier never matches one WITH a
480/// modifier — the distinguishing modifier is what does the referring.
481fn modifier_sets_match(a: &[Symbol], b: &[Symbol]) -> bool {
482    if a.len() != b.len() || a.is_empty() {
483        return false;
484    }
485    a.iter().all(|m| b.contains(m)) && b.iter().all(|m| a.contains(m))
486}
487
488#[derive(Debug, Clone)]
489pub struct Referent {
490    pub variable: Symbol,
491    pub noun_class: Symbol,
492    pub gender: Gender,
493    pub number: Number,
494    pub source: ReferentSource,
495    pub used_by_pronoun: bool,
496    pub ownership: OwnershipState,
497    /// The distinguishing modifier(s) of the description that introduced this
498    /// referent — the adjective/gerund that does the REFERRING in "the
499    /// \[modifier\] \[head\]". For "the hunting vacation" this holds `Hunt`. Empty
500    /// for bare descriptions ("the vacation"). Used by context-driven
501    /// coreference: two definite descriptions that share their distinguishing
502    /// modifier AND have sort-compatible head nouns denote the same entity.
503    pub modifiers: Vec<Symbol>,
504}
505
506impl Referent {
507    pub fn new(variable: Symbol, noun_class: Symbol, gender: Gender, number: Number, source: ReferentSource) -> Self {
508        Self {
509            variable,
510            noun_class,
511            gender,
512            number,
513            source,
514            used_by_pronoun: false,
515            ownership: OwnershipState::Owned,
516            modifiers: Vec::new(),
517        }
518    }
519
520    /// Construct a referent carrying its distinguishing modifier(s).
521    pub fn with_modifiers(
522        variable: Symbol,
523        noun_class: Symbol,
524        gender: Gender,
525        number: Number,
526        source: ReferentSource,
527        modifiers: Vec<Symbol>,
528    ) -> Self {
529        let mut referent = Self::new(variable, noun_class, gender, number, source);
530        referent.modifiers = modifiers;
531        referent
532    }
533
534    pub fn should_be_universal(&self) -> bool {
535        self.source.gets_universal_force() || self.used_by_pronoun
536    }
537}
538
539#[derive(Debug, Clone, Default)]
540pub struct DrsBox {
541    pub universe: Vec<Referent>,
542    pub box_type: Option<BoxType>,
543    pub parent: Option<usize>,
544}
545
546impl DrsBox {
547    pub fn new(box_type: BoxType, parent: Option<usize>) -> Self {
548        Self {
549            universe: Vec::new(),
550            box_type: Some(box_type),
551            parent,
552        }
553    }
554}
555
556#[derive(Debug, Clone)]
557pub struct Drs {
558    boxes: Vec<DrsBox>,
559    main_box: usize,
560    current_box: usize,
561    /// Maps a declared ITEM to the CATEGORY noun it was declared under. A
562    /// category declaration ("2001, 2002, 2003, and 2004 are four different
563    /// years.") records `2001 → Year`, `2002 → Year`, … so a later definite
564    /// LABEL ("the 2003 holiday") can recover the item's category and un-fuse
565    /// to the SAME relation the prepositional-phrase form produces ("the
566    /// holiday was in 2003" → In(x, 2003)). Persisting in the shared discourse
567    /// DRS is what makes the two surface forms converge across sentences.
568    item_categories: std::collections::HashMap<Symbol, Symbol>,
569}
570
571impl Drs {
572    pub fn new() -> Self {
573        let main = DrsBox::new(BoxType::Main, None);
574        Self {
575            boxes: vec![main],
576            main_box: 0,
577            current_box: 0,
578            item_categories: std::collections::HashMap::new(),
579        }
580    }
581
582    pub fn enter_box(&mut self, box_type: BoxType) -> usize {
583        let parent = self.current_box;
584        let new_box = DrsBox::new(box_type, Some(parent));
585        let idx = self.boxes.len();
586        self.boxes.push(new_box);
587        self.current_box = idx;
588        idx
589    }
590
591    pub fn exit_box(&mut self) {
592        if let Some(parent) = self.boxes[self.current_box].parent {
593            self.current_box = parent;
594        }
595    }
596
597    pub fn current_box_index(&self) -> usize {
598        self.current_box
599    }
600
601    pub fn current_box_type(&self) -> Option<BoxType> {
602        self.boxes.get(self.current_box).and_then(|b| b.box_type)
603    }
604
605    pub fn introduce_referent(&mut self, variable: Symbol, noun_class: Symbol, gender: Gender, number: Number) {
606        let source = self.boxes[self.current_box]
607            .box_type
608            .map(|bt| bt.to_referent_source())
609            .unwrap_or(ReferentSource::MainClause);
610
611        let referent = Referent::new(variable, noun_class, gender, number, source);
612        self.boxes[self.current_box].universe.push(referent);
613    }
614
615    /// Introduce a referent with an explicit source (used for negative quantifiers like "No X")
616    pub fn introduce_referent_with_source(&mut self, variable: Symbol, noun_class: Symbol, gender: Gender, number: Number, source: ReferentSource) {
617        let referent = Referent::new(variable, noun_class, gender, number, source);
618        self.boxes[self.current_box].universe.push(referent);
619    }
620
621    /// Introduce a referent carrying its distinguishing modifier(s) (the
622    /// adjective/gerund that does the referring in "the \[modifier\] \[head\]").
623    /// These modifiers feed context-driven coreference for definite
624    /// descriptions — see [`Drs::resolve_definite_by_modifier`].
625    pub fn introduce_referent_with_modifiers(
626        &mut self,
627        variable: Symbol,
628        noun_class: Symbol,
629        gender: Gender,
630        number: Number,
631        source: ReferentSource,
632        modifiers: Vec<Symbol>,
633    ) {
634        let referent =
635            Referent::with_modifiers(variable, noun_class, gender, number, source, modifiers);
636        self.boxes[self.current_box].universe.push(referent);
637    }
638
639    /// Accommodate a referent at the MAIN (highest) box. Definites presuppose
640    /// existence, so they project globally even when first mentioned inside
641    /// an embedded box (Van der Sandt global accommodation).
642    pub fn introduce_referent_global(&mut self, variable: Symbol, noun_class: Symbol, gender: Gender, number: Number, source: ReferentSource) {
643        let referent = Referent::new(variable, noun_class, gender, number, source);
644        self.boxes[self.main_box].universe.push(referent);
645    }
646
647    pub fn introduce_proper_name(&mut self, variable: Symbol, name: Symbol, gender: Gender) {
648        // Proper names are always singular
649        let referent = Referent::new(variable, name, gender, Number::Singular, ReferentSource::ProperName);
650        self.boxes[self.current_box].universe.push(referent);
651    }
652
653    /// Is this symbol a RIGID referent (proper name or deictic constant)?
654    /// A pronoun resolving to a rigid referent denotes the constant itself,
655    /// not a discourse-bound variable.
656    pub fn is_rigid_referent(&self, symbol: Symbol) -> bool {
657        self.boxes.iter().any(|b| {
658            b.universe
659                .iter()
660                .any(|r| r.variable == symbol && r.source == ReferentSource::ProperName)
661        })
662    }
663
664    /// Record that `item` was declared under category noun `category`. Called
665    /// for each member of a coordinated category declaration; read back by a
666    /// definite-description label to drive category-based un-fusing.
667    pub fn register_item_category(&mut self, item: Symbol, category: Symbol) {
668        self.item_categories.insert(item, category);
669    }
670
671    /// The category noun `item` was declared under, if any.
672    pub fn item_category(&self, item: Symbol) -> Option<Symbol> {
673        self.item_categories.get(&item).copied()
674    }
675
676    /// Check if a referent in box `from_box` can access referents in box `target_box`
677    pub fn is_accessible(&self, target_box: usize, from_box: usize) -> bool {
678        if target_box == from_box {
679            return true;
680        }
681
682        let target = &self.boxes[target_box];
683        let from = &self.boxes[from_box];
684
685        // Check target box type - some boxes block outward access
686        // The "accessibility" principle in DRT:
687        // - Reality cannot see into hypotheticals (ModalScope)
688        // - Affirmative cannot see into negative (NegationScope)
689        // - One disjunct cannot see into another (Disjunct)
690        if let Some(bt) = target.box_type {
691            match bt {
692                BoxType::NegationScope | BoxType::Disjunct | BoxType::ModalScope => {
693                    // These boxes are NOT accessible from outside
694                    // A wolf in imagination cannot be seen from reality
695                    return false;
696                }
697                _ => {}
698            }
699        }
700
701        // Check if from_box can see target_box
702        // Consequent can see antecedent
703        if let (Some(BoxType::ConditionalConsequent), Some(BoxType::ConditionalAntecedent)) =
704            (from.box_type, target.box_type)
705        {
706            // Check if they share the same parent (same conditional)
707            if from.parent == target.parent {
708                return true;
709            }
710        }
711
712        // Universal scope can see universal restrictor
713        if let (Some(BoxType::UniversalScope), Some(BoxType::UniversalRestrictor)) =
714            (from.box_type, target.box_type)
715        {
716            if from.parent == target.parent {
717                return true;
718            }
719        }
720
721        // Can always access ancestors (parent chain)
722        let mut current = from_box;
723        while let Some(parent) = self.boxes[current].parent {
724            if parent == target_box {
725                return true;
726            }
727            current = parent;
728        }
729
730        false
731    }
732
733    /// Resolve a pronoun by finding accessible referents matching gender and number
734    pub fn resolve_pronoun(&mut self, from_box: usize, gender: Gender, number: Number) -> Result<Symbol, ScopeError> {
735        // Phase 1: Search accessible referents
736        // A referent is accessible if:
737        //   - It's in an accessible box, OR
738        //   - It has MainClause/ProperName source (globally accessible, e.g. definite descriptions)
739        // Skip referents from NegationScope or Disjunct sources (always inaccessible)
740        let mut candidates = Vec::new();
741
742        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
743            let box_accessible = self.is_accessible(box_idx, from_box);
744
745            for referent in &drs_box.universe {
746                // Skip referents that are from negative quantifiers (No X) or disjuncts
747                // Both are inaccessible outward per DRS accessibility
748                if matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct) {
749                    continue;
750                }
751
752                // Check if this referent is accessible:
753                // Either the box is accessible, or the referent has globally accessible source
754                let has_global_source = matches!(referent.source, ReferentSource::MainClause | ReferentSource::ProperName);
755                if !box_accessible && !has_global_source {
756                    continue;
757                }
758
759                // Gender matching rules:
760                // - Exact match (Male=Male, Female=Female, etc)
761                // - Unknown referents match any pronoun (gender accommodation)
762                // - Unknown pronouns match any referent
763                // This allows "He" to refer to "farmer" even if farmer's gender is Unknown
764                let gender_match = referent.gender == gender
765                    || referent.gender == Gender::Unknown
766                    || gender == Gender::Unknown;
767
768                // Number matching: must match exactly (no number accommodation)
769                let number_match = referent.number == number;
770
771                if gender_match && number_match {
772                    candidates.push((box_idx, referent.variable));
773                }
774            }
775        }
776
777        // If found in accessible scope, return success
778        if let Some((box_idx, var)) = candidates.last() {
779            let box_idx = *box_idx;
780            let var = *var;
781            for referent in &mut self.boxes[box_idx].universe {
782                if referent.variable == var {
783                    referent.used_by_pronoun = true;
784                    return Ok(var);
785                }
786            }
787        }
788
789        // Phase 2: Check inaccessible boxes OR referents with NegationScope/Disjunct source
790        // Use the same strict gender matching for consistency
791        for (_box_idx, drs_box) in self.boxes.iter().enumerate() {
792            for referent in &drs_box.universe {
793                // Referents with MainClause or ProperName source are ALWAYS accessible
794                // (definite descriptions presuppose existence and are globally accessible)
795                if matches!(referent.source, ReferentSource::MainClause | ReferentSource::ProperName) {
796                    continue;
797                }
798
799                // Check for referents with NegationScope/Disjunct source (from "No X" or disjuncts)
800                // OR referents in inaccessible boxes
801                let is_inaccessible = matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct)
802                    || !self.is_accessible(_box_idx, from_box);
803
804                if is_inaccessible {
805                    // Same matching as Phase 1
806                    let gender_match = referent.gender == gender
807                        || (gender == Gender::Unknown)
808                        || (gender == Gender::Neuter && referent.gender == Gender::Unknown);
809                    let number_match = referent.number == number;
810
811                    if gender_match && number_match {
812                        // Found a matching referent but it's inaccessible
813                        let blocking_scope = if matches!(referent.source, ReferentSource::NegationScope) {
814                            BoxType::NegationScope
815                        } else if matches!(referent.source, ReferentSource::Disjunct) {
816                            BoxType::Disjunct
817                        } else {
818                            drs_box.box_type.unwrap_or(BoxType::Main)
819                        };
820                        let noun_class_str = format!("{:?}", referent.noun_class);
821                        return Err(ScopeError::InaccessibleReferent {
822                            gender,
823                            blocking_scope,
824                            reason: format!("'{}' is trapped in {:?} scope and cannot be accessed",
825                                noun_class_str, blocking_scope),
826                        });
827                    }
828                }
829            }
830        }
831
832        // Phase 3: Not found anywhere
833        Err(ScopeError::NoMatchingReferent {
834            gender,
835            number,
836        })
837    }
838
839    /// Resolve a definite description by finding accessible referent matching noun class
840    pub fn resolve_definite(&self, from_box: usize, noun_class: Symbol) -> Option<Symbol> {
841        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
842            if self.is_accessible(box_idx, from_box) {
843                for referent in drs_box.universe.iter().rev() {
844                    if referent.noun_class == noun_class {
845                        return Some(referent.variable);
846                    }
847                }
848            }
849        }
850        None
851    }
852
853    /// Context-driven coreference for MODIFIED definite descriptions.
854    ///
855    /// When an exact head-noun match fails, two definite descriptions still
856    /// corefer when (a) their distinguishing modifier matches AND (b) their
857    /// head nouns are of a COMPATIBLE occasion sort. This is the
858    /// modifier-does-the-referring principle: in "the hunting vacation" /
859    /// "the hunting trip" the modifier `Hunt` identifies the entity and the
860    /// head noun (vacation/trip) is a soft type, so both descriptions denote
861    /// the same discourse referent.
862    ///
863    /// This is NOT synonymy — no `Vacation ↔ Trip` axiom is asserted; the FOL
864    /// keeps `Vacation(x)` and `Trip(x)` literally. Coreference is purely a
865    /// referent-resolution step: the later description reuses the earlier
866    /// referent's variable.
867    ///
868    /// Guardrails (enforced here):
869    /// - The modifier sets must match EXACTLY (differing modifiers — "the
870    ///   hunting trip" vs "the skydiving trip" — never corefer), and a bare
871    ///   (modifier-less) description never coreferes through this path.
872    /// - BOTH head nouns must be of an OCCASION sort (`Sort::is_occasion`)
873    ///   and mutually sort-compatible. Concrete objects ("the red box" / "the
874    ///   red ball", both `Physical`) are excluded, so a shared modifier on
875    ///   non-occasion nouns never triggers coreference.
876    pub fn resolve_definite_by_modifier(
877        &self,
878        interner: &crate::Interner,
879        from_box: usize,
880        noun_class: Symbol,
881        modifiers: &[Symbol],
882    ) -> Option<Symbol> {
883        use crate::lexicon::lookup_sort;
884
885        // A modifier-less description cannot refer through its (absent)
886        // modifier. Conservative: no modifier ⇒ no coreference via this path.
887        if modifiers.is_empty() {
888            return None;
889        }
890
891        // The new description's head noun must itself be an occasion sort —
892        // otherwise the modifier-does-the-referring principle does not apply.
893        let new_sort = lookup_sort(interner.resolve(noun_class))?;
894        if !new_sort.is_occasion() {
895            return None;
896        }
897
898        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
899            if !self.is_accessible(box_idx, from_box) {
900                continue;
901            }
902            for referent in drs_box.universe.iter().rev() {
903                // The distinguishing modifier(s) must match exactly. Order is
904                // irrelevant; the set of modifier symbols must be equal.
905                if !modifier_sets_match(&referent.modifiers, modifiers) {
906                    continue;
907                }
908
909                // The prior referent's head noun must also be an occasion sort
910                // and mutually sort-compatible with the new description's head.
911                let Some(prior_sort) = lookup_sort(interner.resolve(referent.noun_class)) else {
912                    continue;
913                };
914                if !prior_sort.is_occasion() {
915                    continue;
916                }
917                if !new_sort.is_compatible_with(prior_sort)
918                    && !prior_sort.is_compatible_with(new_sort)
919                {
920                    continue;
921                }
922
923                return Some(referent.variable);
924            }
925        }
926        None
927    }
928
929    /// Check if a referent exists by variable name (for imperative mode variable validation)
930    pub fn has_referent_by_variable(&self, var: Symbol) -> bool {
931        for drs_box in &self.boxes {
932            for referent in &drs_box.universe {
933                if referent.variable == var {
934                    return true;
935                }
936            }
937        }
938        false
939    }
940
941    /// Resolve bridging anaphora by finding referents whose type contains the noun as a part.
942    /// Returns matching referent and whole name for PartOf relation.
943    pub fn resolve_bridging(&self, interner: &crate::Interner, noun_class: Symbol) -> Option<(Symbol, &'static str)> {
944        use crate::ontology::find_bridging_wholes;
945
946        let noun_str = interner.resolve(noun_class);
947        let Some(wholes) = find_bridging_wholes(noun_str) else {
948            return None;
949        };
950
951        // Look for a referent whose noun_class matches one of the possible wholes
952        for whole in wholes {
953            for drs_box in &self.boxes {
954                for referent in drs_box.universe.iter().rev() {
955                    let ref_class_str = interner.resolve(referent.noun_class);
956                    if ref_class_str.eq_ignore_ascii_case(whole) {
957                        return Some((referent.variable, *whole));
958                    }
959                }
960            }
961        }
962        None
963    }
964
965    /// Get all referents that should receive universal quantification
966    pub fn get_universal_referents(&self) -> Vec<Symbol> {
967        let mut result = Vec::new();
968        for drs_box in &self.boxes {
969            for referent in &drs_box.universe {
970                // Proper names are rigid constants, never universally bound — a
971                // counterfactual "If John had studied…" must not become ∀John.
972                if referent.should_be_universal()
973                    && !matches!(referent.source, ReferentSource::ProperName)
974                {
975                    result.push(referent.variable);
976                }
977            }
978        }
979        result
980    }
981
982    /// Get all referents that should receive existential quantification
983    pub fn get_existential_referents(&self) -> Vec<Symbol> {
984        let mut result = Vec::new();
985        for drs_box in &self.boxes {
986            for referent in &drs_box.universe {
987                if !referent.should_be_universal()
988                    && !matches!(referent.source, ReferentSource::ProperName)
989                {
990                    result.push(referent.variable);
991                }
992            }
993        }
994        result
995    }
996
997    /// Get the most recent event referent (for binding weather adjectives to events)
998    pub fn get_last_event_referent(&self, interner: &crate::intern::Interner) -> Option<Symbol> {
999        // Search all boxes in reverse order for event referents
1000        for drs_box in self.boxes.iter().rev() {
1001            for referent in drs_box.universe.iter().rev() {
1002                let class_str = interner.resolve(referent.noun_class);
1003                if class_str == "Event" {
1004                    return Some(referent.variable);
1005                }
1006            }
1007        }
1008        None
1009    }
1010
1011    /// Check if we're currently in a conditional antecedent
1012    pub fn in_conditional_antecedent(&self) -> bool {
1013        matches!(
1014            self.boxes.get(self.current_box).and_then(|b| b.box_type),
1015            Some(BoxType::ConditionalAntecedent)
1016        )
1017    }
1018
1019    /// Check if we're currently in a universal restrictor
1020    pub fn in_universal_restrictor(&self) -> bool {
1021        matches!(
1022            self.boxes.get(self.current_box).and_then(|b| b.box_type),
1023            Some(BoxType::UniversalRestrictor)
1024        )
1025    }
1026
1027    /// Get all referents that can telescope across sentence boundaries.
1028    /// Only includes referents from boxes where can_telescope() is true.
1029    /// Excludes referents blocked by negation or disjunction.
1030    pub fn get_telescope_candidates(&self) -> Vec<TelescopeCandidate> {
1031        let mut candidates = Vec::new();
1032
1033        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
1034            // Check if this box type allows telescoping
1035            if let Some(box_type) = drs_box.box_type {
1036                if !box_type.can_telescope() {
1037                    continue; // Skip negation and disjunction boxes
1038                }
1039            }
1040
1041            // Check if this box is blocked by an ancestor negation/disjunction
1042            let mut is_blocked = false;
1043            let mut check_idx = box_idx;
1044            while let Some(parent_idx) = self.boxes.get(check_idx).and_then(|b| b.parent) {
1045                if let Some(parent_type) = self.boxes.get(parent_idx).and_then(|b| b.box_type) {
1046                    if parent_type.blocks_accessibility() {
1047                        is_blocked = true;
1048                        break;
1049                    }
1050                }
1051                check_idx = parent_idx;
1052            }
1053
1054            if is_blocked {
1055                continue;
1056            }
1057
1058            // Collect referents from this box (skip those with blocking sources)
1059            let is_modal_box = drs_box.box_type == Some(BoxType::ModalScope);
1060            for referent in &drs_box.universe {
1061                // Skip referents that are marked with NegationScope or Disjunct source
1062                // These are trapped inside negation/disjunction and cannot telescope
1063                if matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct) {
1064                    continue;
1065                }
1066
1067                candidates.push(TelescopeCandidate {
1068                    variable: referent.variable,
1069                    noun_class: referent.noun_class,
1070                    gender: referent.gender,
1071                    origin_box: box_idx,
1072                    scope_path: Vec::new(), // TODO: Track scope path during parsing
1073                    in_modal_scope: is_modal_box || referent.source == ReferentSource::ModalScope,
1074                });
1075            }
1076        }
1077
1078        candidates
1079    }
1080
1081    /// Find a referent that matches but is blocked by scope.
1082    /// Used to generate informative error messages.
1083    pub fn find_blocked_referent(&self, from_box: usize, gender: Gender) -> Option<(Symbol, BoxType)> {
1084        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
1085            // Only check boxes that are NOT accessible
1086            if self.is_accessible(box_idx, from_box) {
1087                continue;
1088            }
1089
1090            // Check if this box type blocks access
1091            if let Some(box_type) = drs_box.box_type {
1092                if box_type.blocks_accessibility() {
1093                    for referent in &drs_box.universe {
1094                        let gender_match = gender == Gender::Unknown
1095                            || referent.gender == Gender::Unknown
1096                            || referent.gender == gender
1097                            || gender == Gender::Neuter;
1098
1099                        if gender_match {
1100                            return Some((referent.variable, box_type));
1101                        }
1102                    }
1103                }
1104            }
1105        }
1106        None
1107    }
1108
1109    /// Set ownership state for a referent by noun class
1110    pub fn set_ownership(&mut self, noun_class: Symbol, state: OwnershipState) {
1111        for drs_box in &mut self.boxes {
1112            for referent in &mut drs_box.universe {
1113                if referent.noun_class == noun_class {
1114                    referent.ownership = state;
1115                    return;
1116                }
1117            }
1118        }
1119    }
1120
1121    /// Set ownership state for a referent by variable name
1122    pub fn set_ownership_by_var(&mut self, var: Symbol, state: OwnershipState) {
1123        for drs_box in &mut self.boxes {
1124            for referent in &mut drs_box.universe {
1125                if referent.variable == var {
1126                    referent.ownership = state;
1127                    return;
1128                }
1129            }
1130        }
1131    }
1132
1133    /// Get ownership state for a referent by noun class
1134    pub fn get_ownership(&self, noun_class: Symbol) -> Option<OwnershipState> {
1135        for drs_box in &self.boxes {
1136            for referent in &drs_box.universe {
1137                if referent.noun_class == noun_class {
1138                    return Some(referent.ownership);
1139                }
1140            }
1141        }
1142        None
1143    }
1144
1145    /// Get ownership state for a referent by variable name
1146    pub fn get_ownership_by_var(&self, var: Symbol) -> Option<OwnershipState> {
1147        for drs_box in &self.boxes {
1148            for referent in &drs_box.universe {
1149                if referent.variable == var {
1150                    return Some(referent.ownership);
1151                }
1152            }
1153        }
1154        None
1155    }
1156
1157    pub fn clear(&mut self) {
1158        self.boxes.clear();
1159        let main = DrsBox::new(BoxType::Main, None);
1160        self.boxes.push(main);
1161        self.main_box = 0;
1162        self.current_box = 0;
1163        self.item_categories.clear();
1164    }
1165}
1166
1167impl Default for Drs {
1168    fn default() -> Self {
1169        Self::new()
1170    }
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176    use logicaffeine_base::Interner;
1177
1178    #[test]
1179    fn referent_source_universal_force() {
1180        assert!(ReferentSource::ConditionalAntecedent.gets_universal_force());
1181        assert!(ReferentSource::UniversalRestrictor.gets_universal_force());
1182        assert!(!ReferentSource::MainClause.gets_universal_force());
1183        assert!(!ReferentSource::ProperName.gets_universal_force());
1184    }
1185
1186    #[test]
1187    fn drs_new_has_main_box() {
1188        let drs = Drs::new();
1189        assert_eq!(drs.boxes.len(), 1);
1190        assert_eq!(drs.current_box, 0);
1191        assert_eq!(drs.boxes[0].box_type, Some(BoxType::Main));
1192    }
1193
1194    #[test]
1195    fn drs_enter_exit_box() {
1196        let mut drs = Drs::new();
1197        assert_eq!(drs.current_box, 0);
1198
1199        let ant_idx = drs.enter_box(BoxType::ConditionalAntecedent);
1200        assert_eq!(ant_idx, 1);
1201        assert_eq!(drs.current_box, 1);
1202        assert_eq!(drs.boxes[1].parent, Some(0));
1203
1204        drs.exit_box();
1205        assert_eq!(drs.current_box, 0);
1206    }
1207
1208    #[test]
1209    fn drs_introduce_referent_tracks_source() {
1210        let mut interner = Interner::new();
1211        let mut drs = Drs::new();
1212
1213        let x = interner.intern("x");
1214        let farmer = interner.intern("Farmer");
1215
1216        // In main box - should be MainClause
1217        drs.introduce_referent(x, farmer, Gender::Male, Number::Singular);
1218        assert_eq!(drs.boxes[0].universe[0].source, ReferentSource::MainClause);
1219
1220        // Enter conditional antecedent
1221        drs.enter_box(BoxType::ConditionalAntecedent);
1222        let y = interner.intern("y");
1223        let donkey = interner.intern("Donkey");
1224        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1225        assert_eq!(
1226            drs.boxes[1].universe[0].source,
1227            ReferentSource::ConditionalAntecedent
1228        );
1229    }
1230
1231    #[test]
1232    fn drs_conditional_antecedent_accessible_from_consequent() {
1233        let mut interner = Interner::new();
1234        let mut drs = Drs::new();
1235
1236        // Enter conditional antecedent
1237        let ant_idx = drs.enter_box(BoxType::ConditionalAntecedent);
1238        let y = interner.intern("y");
1239        let donkey = interner.intern("Donkey");
1240        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1241        drs.exit_box();
1242
1243        // Enter conditional consequent
1244        let cons_idx = drs.enter_box(BoxType::ConditionalConsequent);
1245
1246        // Consequent should be able to access antecedent
1247        assert!(drs.is_accessible(ant_idx, cons_idx));
1248    }
1249
1250    #[test]
1251    fn drs_negation_blocks_accessibility() {
1252        let mut drs = Drs::new();
1253
1254        // Enter negation scope
1255        let neg_idx = drs.enter_box(BoxType::NegationScope);
1256        drs.exit_box();
1257
1258        // Main box should NOT be able to access negation scope
1259        assert!(!drs.is_accessible(neg_idx, 0));
1260    }
1261
1262    #[test]
1263    fn drs_get_universal_referents() {
1264        let mut interner = Interner::new();
1265        let mut drs = Drs::new();
1266
1267        let x = interner.intern("x");
1268        let farmer = interner.intern("Farmer");
1269        drs.introduce_referent(x, farmer, Gender::Male, Number::Singular);
1270
1271        drs.enter_box(BoxType::ConditionalAntecedent);
1272        let y = interner.intern("y");
1273        let donkey = interner.intern("Donkey");
1274        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1275
1276        let universals = drs.get_universal_referents();
1277        assert_eq!(universals.len(), 1);
1278        assert_eq!(universals[0], y);
1279    }
1280
1281    #[test]
1282    fn drs_pronoun_resolution_marks_used() {
1283        let mut interner = Interner::new();
1284        let mut drs = Drs::new();
1285
1286        drs.enter_box(BoxType::UniversalRestrictor);
1287        let y = interner.intern("y");
1288        let donkey = interner.intern("Donkey");
1289        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1290
1291        // Resolve "it" - should find donkey
1292        let resolved = drs.resolve_pronoun(drs.current_box, Gender::Neuter, Number::Singular);
1293        assert_eq!(resolved, Ok(y));
1294
1295        // Should be marked as used
1296        assert!(drs.boxes[1].universe[0].used_by_pronoun);
1297    }
1298}