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        eprintln!("[TELESCOPE DEBUG] can_access_modal={}, candidates={:?}",
288            can_access_modal,
289            self.telescope_candidates.iter()
290                .map(|c| (c.in_modal_scope, c.gender))
291                .collect::<Vec<_>>()
292        );
293
294        // Apply same Gender Accommodation rules as resolve_pronoun:
295        // - Exact match (Male=Male, Female=Female, etc)
296        // - Unknown referent matches any pronoun (Gender Accommodation)
297        // - Unknown pronoun matches any referent
298        for candidate in &self.telescope_candidates {
299            // MODAL BARRIER CHECK: Skip hypothetical entities when in reality mode
300            if candidate.in_modal_scope && !can_access_modal {
301                // Wolf in imagination cannot be referenced from reality
302                #[cfg(debug_assertions)]
303                eprintln!("[TELESCOPE DEBUG] BLOCKED modal candidate: {:?}", candidate.gender);
304                continue;
305            }
306
307            let gender_match = candidate.gender == gender
308                || candidate.gender == Gender::Unknown  // Gender Accommodation
309                || gender == Gender::Unknown;
310
311            if gender_match {
312                return Some(candidate.clone());
313            }
314        }
315
316        None
317    }
318
319    /// Set ownership state for a referent by noun class
320    pub fn set_ownership(&mut self, noun_class: Symbol, state: OwnershipState) {
321        self.drs.set_ownership(noun_class, state);
322    }
323
324    /// Get ownership state for a referent by noun class
325    pub fn get_ownership(&self, noun_class: Symbol) -> Option<OwnershipState> {
326        self.drs.get_ownership(noun_class)
327    }
328
329    /// Set ownership state for a referent by variable name
330    pub fn set_ownership_by_var(&mut self, var: Symbol, state: OwnershipState) {
331        self.drs.set_ownership_by_var(var, state);
332    }
333
334    /// Get ownership state for a referent by variable name
335    pub fn get_ownership_by_var(&self, var: Symbol) -> Option<OwnershipState> {
336        self.drs.get_ownership_by_var(var)
337    }
338
339    // ============================================
340    // MODAL SUBORDINATION METHODS
341    // ============================================
342
343    /// Enter a modal context (e.g., "might", "would", "could")
344    pub fn enter_modal_context(&mut self, is_epistemic: bool, force: f32) {
345        self.current_modal_context = Some(ModalContext {
346            active: true,
347            is_epistemic,
348            force,
349        });
350        // Also enter a modal box in the DRS
351        self.drs.enter_box(BoxType::ModalScope);
352    }
353
354    /// Exit the current modal context
355    pub fn exit_modal_context(&mut self) {
356        self.current_modal_context = None;
357        self.drs.exit_box();
358    }
359
360    /// Check if we're currently in a modal context
361    pub fn in_modal_context(&self) -> bool {
362        self.current_modal_context.is_some()
363    }
364
365    /// Check if there's a prior modal context for subordination
366    pub fn has_prior_modal_context(&self) -> bool {
367        self.prior_modal_context.is_some()
368    }
369
370    /// Check if current modal can subordinate to prior context
371    /// "would" can continue a "might" world
372    pub fn can_subordinate(&self) -> bool {
373        self.prior_modal_context.is_some()
374    }
375
376    /// Clear the world state (reset for new discourse)
377    pub fn clear(&mut self) {
378        self.drs.clear();
379        self.event_counter = 0;
380        self.event_history.clear();
381        self.reference_time_counter = 0;
382        self.current_reference_time = None;
383        self.time_constraints.clear();
384        self.telescope_candidates.clear();
385        self.discourse_mode = false;
386        self.current_modal_context = None;
387        self.prior_modal_context = None;
388    }
389}
390
391impl Default for WorldState {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397// ============================================
398// REFERENT SOURCE
399// ============================================
400
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402pub enum ReferentSource {
403    /// Indefinite in main clause - gets existential force
404    MainClause,
405    /// Proper name - no quantifier (constant)
406    ProperName,
407    /// Indefinite in conditional antecedent - gets universal force (DRS signature)
408    ConditionalAntecedent,
409    /// Indefinite in universal restrictor (relative clause) - gets universal force
410    UniversalRestrictor,
411    /// Inside negation scope - inaccessible outward
412    NegationScope,
413    /// Inside disjunction - inaccessible outward
414    Disjunct,
415    /// Inside modal scope - accessible via modal subordination
416    ModalScope,
417}
418
419impl ReferentSource {
420    pub fn gets_universal_force(&self) -> bool {
421        matches!(
422            self,
423            ReferentSource::ConditionalAntecedent | ReferentSource::UniversalRestrictor
424        )
425    }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum BoxType {
430    /// Top-level discourse box
431    Main,
432    /// Antecedent of conditional ("if" clause)
433    ConditionalAntecedent,
434    /// Consequent of conditional ("then" clause)
435    ConditionalConsequent,
436    /// Scope of negation
437    NegationScope,
438    /// Restrictor of universal quantifier (relative clause in "every X who...")
439    UniversalRestrictor,
440    /// Nuclear scope of universal quantifier
441    UniversalScope,
442    /// Branch of disjunction
443    Disjunct,
444    /// Scope of modal operator (might, would, could, etc.)
445    /// Allows modal subordination: pronouns can access referents via telescoping
446    ModalScope,
447}
448
449impl BoxType {
450    pub fn to_referent_source(&self) -> ReferentSource {
451        match self {
452            BoxType::Main => ReferentSource::MainClause,
453            BoxType::ConditionalAntecedent => ReferentSource::ConditionalAntecedent,
454            BoxType::ConditionalConsequent => ReferentSource::MainClause,
455            BoxType::NegationScope => ReferentSource::NegationScope,
456            BoxType::UniversalRestrictor => ReferentSource::UniversalRestrictor,
457            BoxType::UniversalScope => ReferentSource::MainClause,
458            BoxType::Disjunct => ReferentSource::Disjunct,
459            BoxType::ModalScope => ReferentSource::ModalScope,
460        }
461    }
462
463    /// Can referents in this box be accessed via telescoping across sentence boundaries?
464    /// Universal quantifiers, conditionals, and modals CAN telescope.
465    /// Negation and disjunction CANNOT telescope.
466    pub fn can_telescope(&self) -> bool {
467        matches!(
468            self,
469            BoxType::Main
470            | BoxType::UniversalScope
471            | BoxType::UniversalRestrictor
472            | BoxType::ConditionalConsequent
473            | BoxType::ConditionalAntecedent
474            | BoxType::ModalScope  // Modal subordination allows cross-sentence access
475        )
476        // NegationScope and Disjunct return false implicitly
477    }
478
479    /// Does this box type block accessibility from outside?
480    pub fn blocks_accessibility(&self) -> bool {
481        matches!(self, BoxType::NegationScope | BoxType::Disjunct)
482    }
483}
484
485#[derive(Debug, Clone)]
486pub struct Referent {
487    pub variable: Symbol,
488    pub noun_class: Symbol,
489    pub gender: Gender,
490    pub number: Number,
491    pub source: ReferentSource,
492    pub used_by_pronoun: bool,
493    pub ownership: OwnershipState,
494}
495
496impl Referent {
497    pub fn new(variable: Symbol, noun_class: Symbol, gender: Gender, number: Number, source: ReferentSource) -> Self {
498        Self {
499            variable,
500            noun_class,
501            gender,
502            number,
503            source,
504            used_by_pronoun: false,
505            ownership: OwnershipState::Owned,
506        }
507    }
508
509    pub fn should_be_universal(&self) -> bool {
510        self.source.gets_universal_force() || self.used_by_pronoun
511    }
512}
513
514#[derive(Debug, Clone, Default)]
515pub struct DrsBox {
516    pub universe: Vec<Referent>,
517    pub box_type: Option<BoxType>,
518    pub parent: Option<usize>,
519}
520
521impl DrsBox {
522    pub fn new(box_type: BoxType, parent: Option<usize>) -> Self {
523        Self {
524            universe: Vec::new(),
525            box_type: Some(box_type),
526            parent,
527        }
528    }
529}
530
531#[derive(Debug, Clone)]
532pub struct Drs {
533    boxes: Vec<DrsBox>,
534    main_box: usize,
535    current_box: usize,
536}
537
538impl Drs {
539    pub fn new() -> Self {
540        let main = DrsBox::new(BoxType::Main, None);
541        Self {
542            boxes: vec![main],
543            main_box: 0,
544            current_box: 0,
545        }
546    }
547
548    pub fn enter_box(&mut self, box_type: BoxType) -> usize {
549        let parent = self.current_box;
550        let new_box = DrsBox::new(box_type, Some(parent));
551        let idx = self.boxes.len();
552        self.boxes.push(new_box);
553        self.current_box = idx;
554        idx
555    }
556
557    pub fn exit_box(&mut self) {
558        if let Some(parent) = self.boxes[self.current_box].parent {
559            self.current_box = parent;
560        }
561    }
562
563    pub fn current_box_index(&self) -> usize {
564        self.current_box
565    }
566
567    pub fn current_box_type(&self) -> Option<BoxType> {
568        self.boxes.get(self.current_box).and_then(|b| b.box_type)
569    }
570
571    pub fn introduce_referent(&mut self, variable: Symbol, noun_class: Symbol, gender: Gender, number: Number) {
572        let source = self.boxes[self.current_box]
573            .box_type
574            .map(|bt| bt.to_referent_source())
575            .unwrap_or(ReferentSource::MainClause);
576
577        let referent = Referent::new(variable, noun_class, gender, number, source);
578        self.boxes[self.current_box].universe.push(referent);
579    }
580
581    /// Introduce a referent with an explicit source (used for negative quantifiers like "No X")
582    pub fn introduce_referent_with_source(&mut self, variable: Symbol, noun_class: Symbol, gender: Gender, number: Number, source: ReferentSource) {
583        let referent = Referent::new(variable, noun_class, gender, number, source);
584        self.boxes[self.current_box].universe.push(referent);
585    }
586
587    pub fn introduce_proper_name(&mut self, variable: Symbol, name: Symbol, gender: Gender) {
588        // Proper names are always singular
589        let referent = Referent::new(variable, name, gender, Number::Singular, ReferentSource::ProperName);
590        self.boxes[self.current_box].universe.push(referent);
591    }
592
593    /// Check if a referent in box `from_box` can access referents in box `target_box`
594    pub fn is_accessible(&self, target_box: usize, from_box: usize) -> bool {
595        if target_box == from_box {
596            return true;
597        }
598
599        let target = &self.boxes[target_box];
600        let from = &self.boxes[from_box];
601
602        // Check target box type - some boxes block outward access
603        // The "accessibility" principle in DRT:
604        // - Reality cannot see into hypotheticals (ModalScope)
605        // - Affirmative cannot see into negative (NegationScope)
606        // - One disjunct cannot see into another (Disjunct)
607        if let Some(bt) = target.box_type {
608            match bt {
609                BoxType::NegationScope | BoxType::Disjunct | BoxType::ModalScope => {
610                    // These boxes are NOT accessible from outside
611                    // A wolf in imagination cannot be seen from reality
612                    return false;
613                }
614                _ => {}
615            }
616        }
617
618        // Check if from_box can see target_box
619        // Consequent can see antecedent
620        if let (Some(BoxType::ConditionalConsequent), Some(BoxType::ConditionalAntecedent)) =
621            (from.box_type, target.box_type)
622        {
623            // Check if they share the same parent (same conditional)
624            if from.parent == target.parent {
625                return true;
626            }
627        }
628
629        // Universal scope can see universal restrictor
630        if let (Some(BoxType::UniversalScope), Some(BoxType::UniversalRestrictor)) =
631            (from.box_type, target.box_type)
632        {
633            if from.parent == target.parent {
634                return true;
635            }
636        }
637
638        // Can always access ancestors (parent chain)
639        let mut current = from_box;
640        while let Some(parent) = self.boxes[current].parent {
641            if parent == target_box {
642                return true;
643            }
644            current = parent;
645        }
646
647        false
648    }
649
650    /// Resolve a pronoun by finding accessible referents matching gender and number
651    pub fn resolve_pronoun(&mut self, from_box: usize, gender: Gender, number: Number) -> Result<Symbol, ScopeError> {
652        // Phase 1: Search accessible referents
653        // A referent is accessible if:
654        //   - It's in an accessible box, OR
655        //   - It has MainClause/ProperName source (globally accessible, e.g. definite descriptions)
656        // Skip referents from NegationScope or Disjunct sources (always inaccessible)
657        let mut candidates = Vec::new();
658
659        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
660            let box_accessible = self.is_accessible(box_idx, from_box);
661
662            for referent in &drs_box.universe {
663                // Skip referents that are from negative quantifiers (No X) or disjuncts
664                // Both are inaccessible outward per DRS accessibility
665                if matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct) {
666                    continue;
667                }
668
669                // Check if this referent is accessible:
670                // Either the box is accessible, or the referent has globally accessible source
671                let has_global_source = matches!(referent.source, ReferentSource::MainClause | ReferentSource::ProperName);
672                if !box_accessible && !has_global_source {
673                    continue;
674                }
675
676                // Gender matching rules:
677                // - Exact match (Male=Male, Female=Female, etc)
678                // - Unknown referents match any pronoun (gender accommodation)
679                // - Unknown pronouns match any referent
680                // This allows "He" to refer to "farmer" even if farmer's gender is Unknown
681                let gender_match = referent.gender == gender
682                    || referent.gender == Gender::Unknown
683                    || gender == Gender::Unknown;
684
685                // Number matching: must match exactly (no number accommodation)
686                let number_match = referent.number == number;
687
688                if gender_match && number_match {
689                    candidates.push((box_idx, referent.variable));
690                }
691            }
692        }
693
694        // If found in accessible scope, return success
695        if let Some((box_idx, var)) = candidates.last() {
696            let box_idx = *box_idx;
697            let var = *var;
698            for referent in &mut self.boxes[box_idx].universe {
699                if referent.variable == var {
700                    referent.used_by_pronoun = true;
701                    return Ok(var);
702                }
703            }
704        }
705
706        // Phase 2: Check inaccessible boxes OR referents with NegationScope/Disjunct source
707        // Use the same strict gender matching for consistency
708        for (_box_idx, drs_box) in self.boxes.iter().enumerate() {
709            for referent in &drs_box.universe {
710                // Referents with MainClause or ProperName source are ALWAYS accessible
711                // (definite descriptions presuppose existence and are globally accessible)
712                if matches!(referent.source, ReferentSource::MainClause | ReferentSource::ProperName) {
713                    continue;
714                }
715
716                // Check for referents with NegationScope/Disjunct source (from "No X" or disjuncts)
717                // OR referents in inaccessible boxes
718                let is_inaccessible = matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct)
719                    || !self.is_accessible(_box_idx, from_box);
720
721                if is_inaccessible {
722                    // Same matching as Phase 1
723                    let gender_match = referent.gender == gender
724                        || (gender == Gender::Unknown)
725                        || (gender == Gender::Neuter && referent.gender == Gender::Unknown);
726                    let number_match = referent.number == number;
727
728                    if gender_match && number_match {
729                        // Found a matching referent but it's inaccessible
730                        let blocking_scope = if matches!(referent.source, ReferentSource::NegationScope) {
731                            BoxType::NegationScope
732                        } else if matches!(referent.source, ReferentSource::Disjunct) {
733                            BoxType::Disjunct
734                        } else {
735                            drs_box.box_type.unwrap_or(BoxType::Main)
736                        };
737                        let noun_class_str = format!("{:?}", referent.noun_class);
738                        return Err(ScopeError::InaccessibleReferent {
739                            gender,
740                            blocking_scope,
741                            reason: format!("'{}' is trapped in {:?} scope and cannot be accessed",
742                                noun_class_str, blocking_scope),
743                        });
744                    }
745                }
746            }
747        }
748
749        // Phase 3: Not found anywhere
750        Err(ScopeError::NoMatchingReferent {
751            gender,
752            number,
753        })
754    }
755
756    /// Resolve a definite description by finding accessible referent matching noun class
757    pub fn resolve_definite(&self, from_box: usize, noun_class: Symbol) -> Option<Symbol> {
758        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
759            if self.is_accessible(box_idx, from_box) {
760                for referent in drs_box.universe.iter().rev() {
761                    if referent.noun_class == noun_class {
762                        return Some(referent.variable);
763                    }
764                }
765            }
766        }
767        None
768    }
769
770    /// Check if a referent exists by variable name (for imperative mode variable validation)
771    pub fn has_referent_by_variable(&self, var: Symbol) -> bool {
772        for drs_box in &self.boxes {
773            for referent in &drs_box.universe {
774                if referent.variable == var {
775                    return true;
776                }
777            }
778        }
779        false
780    }
781
782    /// Resolve bridging anaphora by finding referents whose type contains the noun as a part.
783    /// Returns matching referent and whole name for PartOf relation.
784    pub fn resolve_bridging(&self, interner: &crate::Interner, noun_class: Symbol) -> Option<(Symbol, &'static str)> {
785        use crate::ontology::find_bridging_wholes;
786
787        let noun_str = interner.resolve(noun_class);
788        let Some(wholes) = find_bridging_wholes(noun_str) else {
789            return None;
790        };
791
792        // Look for a referent whose noun_class matches one of the possible wholes
793        for whole in wholes {
794            for drs_box in &self.boxes {
795                for referent in drs_box.universe.iter().rev() {
796                    let ref_class_str = interner.resolve(referent.noun_class);
797                    if ref_class_str.eq_ignore_ascii_case(whole) {
798                        return Some((referent.variable, *whole));
799                    }
800                }
801            }
802        }
803        None
804    }
805
806    /// Get all referents that should receive universal quantification
807    pub fn get_universal_referents(&self) -> Vec<Symbol> {
808        let mut result = Vec::new();
809        for drs_box in &self.boxes {
810            for referent in &drs_box.universe {
811                if referent.should_be_universal() {
812                    result.push(referent.variable);
813                }
814            }
815        }
816        result
817    }
818
819    /// Get all referents that should receive existential quantification
820    pub fn get_existential_referents(&self) -> Vec<Symbol> {
821        let mut result = Vec::new();
822        for drs_box in &self.boxes {
823            for referent in &drs_box.universe {
824                if !referent.should_be_universal()
825                    && !matches!(referent.source, ReferentSource::ProperName)
826                {
827                    result.push(referent.variable);
828                }
829            }
830        }
831        result
832    }
833
834    /// Get the most recent event referent (for binding weather adjectives to events)
835    pub fn get_last_event_referent(&self, interner: &crate::intern::Interner) -> Option<Symbol> {
836        // Search all boxes in reverse order for event referents
837        for drs_box in self.boxes.iter().rev() {
838            for referent in drs_box.universe.iter().rev() {
839                let class_str = interner.resolve(referent.noun_class);
840                if class_str == "Event" {
841                    return Some(referent.variable);
842                }
843            }
844        }
845        None
846    }
847
848    /// Check if we're currently in a conditional antecedent
849    pub fn in_conditional_antecedent(&self) -> bool {
850        matches!(
851            self.boxes.get(self.current_box).and_then(|b| b.box_type),
852            Some(BoxType::ConditionalAntecedent)
853        )
854    }
855
856    /// Check if we're currently in a universal restrictor
857    pub fn in_universal_restrictor(&self) -> bool {
858        matches!(
859            self.boxes.get(self.current_box).and_then(|b| b.box_type),
860            Some(BoxType::UniversalRestrictor)
861        )
862    }
863
864    /// Get all referents that can telescope across sentence boundaries.
865    /// Only includes referents from boxes where can_telescope() is true.
866    /// Excludes referents blocked by negation or disjunction.
867    pub fn get_telescope_candidates(&self) -> Vec<TelescopeCandidate> {
868        let mut candidates = Vec::new();
869
870        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
871            // Check if this box type allows telescoping
872            if let Some(box_type) = drs_box.box_type {
873                if !box_type.can_telescope() {
874                    continue; // Skip negation and disjunction boxes
875                }
876            }
877
878            // Check if this box is blocked by an ancestor negation/disjunction
879            let mut is_blocked = false;
880            let mut check_idx = box_idx;
881            while let Some(parent_idx) = self.boxes.get(check_idx).and_then(|b| b.parent) {
882                if let Some(parent_type) = self.boxes.get(parent_idx).and_then(|b| b.box_type) {
883                    if parent_type.blocks_accessibility() {
884                        is_blocked = true;
885                        break;
886                    }
887                }
888                check_idx = parent_idx;
889            }
890
891            if is_blocked {
892                continue;
893            }
894
895            // Collect referents from this box (skip those with blocking sources)
896            let is_modal_box = drs_box.box_type == Some(BoxType::ModalScope);
897            for referent in &drs_box.universe {
898                // Skip referents that are marked with NegationScope or Disjunct source
899                // These are trapped inside negation/disjunction and cannot telescope
900                if matches!(referent.source, ReferentSource::NegationScope | ReferentSource::Disjunct) {
901                    continue;
902                }
903
904                candidates.push(TelescopeCandidate {
905                    variable: referent.variable,
906                    noun_class: referent.noun_class,
907                    gender: referent.gender,
908                    origin_box: box_idx,
909                    scope_path: Vec::new(), // TODO: Track scope path during parsing
910                    in_modal_scope: is_modal_box || referent.source == ReferentSource::ModalScope,
911                });
912            }
913        }
914
915        candidates
916    }
917
918    /// Find a referent that matches but is blocked by scope.
919    /// Used to generate informative error messages.
920    pub fn find_blocked_referent(&self, from_box: usize, gender: Gender) -> Option<(Symbol, BoxType)> {
921        for (box_idx, drs_box) in self.boxes.iter().enumerate() {
922            // Only check boxes that are NOT accessible
923            if self.is_accessible(box_idx, from_box) {
924                continue;
925            }
926
927            // Check if this box type blocks access
928            if let Some(box_type) = drs_box.box_type {
929                if box_type.blocks_accessibility() {
930                    for referent in &drs_box.universe {
931                        let gender_match = gender == Gender::Unknown
932                            || referent.gender == Gender::Unknown
933                            || referent.gender == gender
934                            || gender == Gender::Neuter;
935
936                        if gender_match {
937                            return Some((referent.variable, box_type));
938                        }
939                    }
940                }
941            }
942        }
943        None
944    }
945
946    /// Set ownership state for a referent by noun class
947    pub fn set_ownership(&mut self, noun_class: Symbol, state: OwnershipState) {
948        for drs_box in &mut self.boxes {
949            for referent in &mut drs_box.universe {
950                if referent.noun_class == noun_class {
951                    referent.ownership = state;
952                    return;
953                }
954            }
955        }
956    }
957
958    /// Set ownership state for a referent by variable name
959    pub fn set_ownership_by_var(&mut self, var: Symbol, state: OwnershipState) {
960        for drs_box in &mut self.boxes {
961            for referent in &mut drs_box.universe {
962                if referent.variable == var {
963                    referent.ownership = state;
964                    return;
965                }
966            }
967        }
968    }
969
970    /// Get ownership state for a referent by noun class
971    pub fn get_ownership(&self, noun_class: Symbol) -> Option<OwnershipState> {
972        for drs_box in &self.boxes {
973            for referent in &drs_box.universe {
974                if referent.noun_class == noun_class {
975                    return Some(referent.ownership);
976                }
977            }
978        }
979        None
980    }
981
982    /// Get ownership state for a referent by variable name
983    pub fn get_ownership_by_var(&self, var: Symbol) -> Option<OwnershipState> {
984        for drs_box in &self.boxes {
985            for referent in &drs_box.universe {
986                if referent.variable == var {
987                    return Some(referent.ownership);
988                }
989            }
990        }
991        None
992    }
993
994    pub fn clear(&mut self) {
995        self.boxes.clear();
996        let main = DrsBox::new(BoxType::Main, None);
997        self.boxes.push(main);
998        self.main_box = 0;
999        self.current_box = 0;
1000    }
1001}
1002
1003impl Default for Drs {
1004    fn default() -> Self {
1005        Self::new()
1006    }
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011    use super::*;
1012    use logicaffeine_base::Interner;
1013
1014    #[test]
1015    fn referent_source_universal_force() {
1016        assert!(ReferentSource::ConditionalAntecedent.gets_universal_force());
1017        assert!(ReferentSource::UniversalRestrictor.gets_universal_force());
1018        assert!(!ReferentSource::MainClause.gets_universal_force());
1019        assert!(!ReferentSource::ProperName.gets_universal_force());
1020    }
1021
1022    #[test]
1023    fn drs_new_has_main_box() {
1024        let drs = Drs::new();
1025        assert_eq!(drs.boxes.len(), 1);
1026        assert_eq!(drs.current_box, 0);
1027        assert_eq!(drs.boxes[0].box_type, Some(BoxType::Main));
1028    }
1029
1030    #[test]
1031    fn drs_enter_exit_box() {
1032        let mut drs = Drs::new();
1033        assert_eq!(drs.current_box, 0);
1034
1035        let ant_idx = drs.enter_box(BoxType::ConditionalAntecedent);
1036        assert_eq!(ant_idx, 1);
1037        assert_eq!(drs.current_box, 1);
1038        assert_eq!(drs.boxes[1].parent, Some(0));
1039
1040        drs.exit_box();
1041        assert_eq!(drs.current_box, 0);
1042    }
1043
1044    #[test]
1045    fn drs_introduce_referent_tracks_source() {
1046        let mut interner = Interner::new();
1047        let mut drs = Drs::new();
1048
1049        let x = interner.intern("x");
1050        let farmer = interner.intern("Farmer");
1051
1052        // In main box - should be MainClause
1053        drs.introduce_referent(x, farmer, Gender::Male, Number::Singular);
1054        assert_eq!(drs.boxes[0].universe[0].source, ReferentSource::MainClause);
1055
1056        // Enter conditional antecedent
1057        drs.enter_box(BoxType::ConditionalAntecedent);
1058        let y = interner.intern("y");
1059        let donkey = interner.intern("Donkey");
1060        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1061        assert_eq!(
1062            drs.boxes[1].universe[0].source,
1063            ReferentSource::ConditionalAntecedent
1064        );
1065    }
1066
1067    #[test]
1068    fn drs_conditional_antecedent_accessible_from_consequent() {
1069        let mut interner = Interner::new();
1070        let mut drs = Drs::new();
1071
1072        // Enter conditional antecedent
1073        let ant_idx = drs.enter_box(BoxType::ConditionalAntecedent);
1074        let y = interner.intern("y");
1075        let donkey = interner.intern("Donkey");
1076        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1077        drs.exit_box();
1078
1079        // Enter conditional consequent
1080        let cons_idx = drs.enter_box(BoxType::ConditionalConsequent);
1081
1082        // Consequent should be able to access antecedent
1083        assert!(drs.is_accessible(ant_idx, cons_idx));
1084    }
1085
1086    #[test]
1087    fn drs_negation_blocks_accessibility() {
1088        let mut drs = Drs::new();
1089
1090        // Enter negation scope
1091        let neg_idx = drs.enter_box(BoxType::NegationScope);
1092        drs.exit_box();
1093
1094        // Main box should NOT be able to access negation scope
1095        assert!(!drs.is_accessible(neg_idx, 0));
1096    }
1097
1098    #[test]
1099    fn drs_get_universal_referents() {
1100        let mut interner = Interner::new();
1101        let mut drs = Drs::new();
1102
1103        let x = interner.intern("x");
1104        let farmer = interner.intern("Farmer");
1105        drs.introduce_referent(x, farmer, Gender::Male, Number::Singular);
1106
1107        drs.enter_box(BoxType::ConditionalAntecedent);
1108        let y = interner.intern("y");
1109        let donkey = interner.intern("Donkey");
1110        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1111
1112        let universals = drs.get_universal_referents();
1113        assert_eq!(universals.len(), 1);
1114        assert_eq!(universals[0], y);
1115    }
1116
1117    #[test]
1118    fn drs_pronoun_resolution_marks_used() {
1119        let mut interner = Interner::new();
1120        let mut drs = Drs::new();
1121
1122        drs.enter_box(BoxType::UniversalRestrictor);
1123        let y = interner.intern("y");
1124        let donkey = interner.intern("Donkey");
1125        drs.introduce_referent(y, donkey, Gender::Neuter, Number::Singular);
1126
1127        // Resolve "it" - should find donkey
1128        let resolved = drs.resolve_pronoun(drs.current_box, Gender::Neuter, Number::Singular);
1129        assert_eq!(resolved, Ok(y));
1130
1131        // Should be marked as used
1132        assert!(drs.boxes[1].universe[0].used_by_pronoun);
1133    }
1134}