Skip to main content

logicaffeine_language/parser/
modal.rs

1//! Modal verb parsing with Kripke semantics support.
2//!
3//! This module handles modal auxiliaries (can, could, may, might, must, should, would)
4//! and their semantic interpretation using modal vectors that encode:
5//!
6//! - **Domain**: Alethic (possibility/necessity) vs Deontic (permission/obligation)
7//! - **Flavor**: Root (circumstantial) vs Epistemic (knowledge-based)
8//! - **Force**: Possibility (◇) vs Necessity (□)
9//!
10//! # Modal Vector Examples
11//!
12//! | Modal | Default Reading | Alternative Reading |
13//! |-------|-----------------|---------------------|
14//! | can   | Ability (Root)  | Permission (Deontic) |
15//! | may   | Permission (Deontic) | Possibility (Epistemic) |
16//! | must  | Necessity (Root) | Obligation (Deontic) |
17//! | might | Possibility (Epistemic) | - |
18//!
19//! The module also handles aspect chains (perfect "have", progressive "be -ing").
20
21use super::clause::ClauseParsing;
22use super::noun::NounParsing;
23use super::pragmatics::PragmaticsParsing;
24use super::quantifier::QuantifierParsing;
25use super::{ParseResult, Parser};
26use crate::ast::{AspectOperator, LogicExpr, ModalDomain, ModalFlavor, ModalVector, NeoEventData, QuantifierKind, ThematicRole, VoiceOperator, Term};
27use crate::drs::TimeRelation;
28use crate::error::{ParseError, ParseErrorKind};
29use logicaffeine_base::Symbol;
30use crate::lexicon::{Time, Aspect};
31use crate::token::TokenType;
32
33/// Trait for parsing modal verbs and aspect chains.
34///
35/// Provides methods for interpreting modal auxiliaries (can, must, etc.)
36/// with Kripke semantics and handling aspect markers (perfect, progressive).
37pub trait ModalParsing<'a, 'ctx, 'int> {
38    /// Parses a modal verb and its scope content.
39    fn parse_modal(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
40    /// Parses perfect/progressive aspect chain with a symbol subject.
41    fn parse_aspect_chain(&mut self, subject_symbol: Symbol) -> ParseResult<&'a LogicExpr<'a>>;
42    /// Parses perfect/progressive aspect chain with a term subject.
43    fn parse_aspect_chain_with_term(&mut self, subject_term: Term<'a>) -> ParseResult<&'a LogicExpr<'a>>;
44    /// Converts a modal token to its semantic vector (domain, force, flavor).
45    fn token_to_vector(&self, token: &TokenType) -> ModalVector;
46}
47
48impl<'a, 'ctx, 'int> ModalParsing<'a, 'ctx, 'int> for Parser<'a, 'ctx, 'int> {
49    fn parse_modal(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
50        use crate::drs::BoxType;
51
52        let vector = self.token_to_vector(&self.previous().kind.clone());
53
54        // Enter modal box in parser's DRS (not world_state - that's swapped at sentence boundaries)
55        self.drs.enter_box(BoxType::ModalScope);
56
57        if self.check(&TokenType::That) {
58            self.advance();
59        }
60
61        let content = self.parse_sentence()?;
62
63        // Exit modal box
64        self.drs.exit_box();
65
66        Ok(self.ctx.exprs.alloc(LogicExpr::Modal {
67            vector,
68            operand: content,
69        }))
70    }
71
72    fn parse_aspect_chain(&mut self, subject_symbol: Symbol) -> ParseResult<&'a LogicExpr<'a>> {
73        self.parse_aspect_chain_with_term(Term::Constant(subject_symbol))
74    }
75
76    fn parse_aspect_chain_with_term(&mut self, subject_term: Term<'a>) -> ParseResult<&'a LogicExpr<'a>> {
77        let mut has_modal = false;
78        let mut modal_vector = None;
79        let mut has_negation = false;
80        let mut has_perfect = false;
81        let mut has_passive = false;
82        let mut has_progressive = false;
83
84        if self.check(&TokenType::Would) || self.check(&TokenType::Could)
85            || self.check(&TokenType::Must) || self.check(&TokenType::Can)
86            || self.check(&TokenType::Should) || self.check(&TokenType::May)
87            || self.check(&TokenType::Cannot) || self.check(&TokenType::Might)
88            || self.check(&TokenType::Shall) {
89            let modal_token = self.peek().kind.clone();
90            self.advance();
91            has_modal = true;
92            let vector = self.token_to_vector(&modal_token);
93            modal_vector = Some(vector.clone());
94            // Enter modal box in DRS so any new referents are marked as hypothetical
95            // This ensures "A wolf might enter" puts the wolf in a modal scope
96            self.drs.enter_box(crate::drs::BoxType::ModalScope);
97            // Also set modal context on WorldState for cross-sentence tracking
98            // This is used by end_sentence() to mark telescope candidates as modal-sourced
99            let is_epistemic = matches!(vector.flavor, crate::ast::ModalFlavor::Epistemic);
100            self.world_state.enter_modal_context(is_epistemic, vector.force);
101        }
102
103        if self.check(&TokenType::Not) {
104            self.advance();
105            has_negation = true;
106        }
107
108        // "shall never send" / "must never fail" — treat Never as negation
109        if self.check(&TokenType::Never) && !has_negation {
110            self.advance();
111            has_negation = true;
112        }
113
114        // Check for "be able to" periphrastic modal (= can)
115        // This creates a nested modal: "might be able to fly" → ◇◇Fly(x)
116        let mut nested_modal_vector = None;
117        if self.check_content_word() {
118            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
119            if word == "be" {
120                // Look ahead for "able to"
121                if let Some(next1) = self.tokens.get(self.current + 1) {
122                    let next1_word = self.interner.resolve(next1.lexeme).to_lowercase();
123                    if next1_word == "able" {
124                        if let Some(next2) = self.tokens.get(self.current + 2) {
125                            if matches!(next2.kind, TokenType::To) {
126                                // Consume "be able to" - it's a modal meaning "can" (ability)
127                                self.advance(); // consume "be"
128                                self.advance(); // consume "able"
129                                self.advance(); // consume "to"
130                                nested_modal_vector = Some(ModalVector {
131                                    domain: ModalDomain::Alethic,
132                                    force: 0.5, // ability = possibility
133                                    flavor: ModalFlavor::Root, // "be able to" = Root modal (ability)
134                                    modal_base: None,
135                                    ordering_source: None,
136                                });
137                            }
138                        }
139                    }
140                }
141            }
142        }
143
144        if self.check_content_word() {
145            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
146            if word == "have" || word == "has" || word == "had" {
147                self.advance();
148                has_perfect = true;
149            }
150        }
151
152        if self.check(&TokenType::Had) {
153            self.advance();
154            has_perfect = true;
155            // "had" = past perfect: R < S (past reference time)
156            let r_var = self.world_state.next_reference_time();
157            self.world_state.add_time_constraint(r_var, TimeRelation::Precedes, "S".to_string());
158        }
159
160        if self.check_content_word() {
161            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
162            if word == "been" {
163                self.advance();
164
165                if self.check_verb() {
166                    match &self.peek().kind {
167                        TokenType::Verb { aspect: Aspect::Progressive, .. } => {
168                            has_progressive = true;
169                        }
170                        TokenType::Verb { .. } => {
171                            let next_word = self.interner.resolve(self.peek().lexeme);
172                            if next_word.ends_with("ing") {
173                                has_progressive = true;
174                            } else {
175                                has_passive = true;
176                            }
177                        }
178                        _ => {
179                            has_passive = true;
180                        }
181                    }
182                }
183            }
184        }
185
186        if self.check_content_word() {
187            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
188            if word == "being" {
189                self.advance();
190                has_progressive = true;
191            }
192        }
193
194        // Presupposition trigger under the modal/aspect chain: "Mary might
195        // regret lying." The presupposition PROJECTS out of the modal (Van
196        // der Sandt global accommodation) — the modal wraps only the
197        // assertion: Presup(◇Regret(m), ⟨Lied(m)⟩).
198        if self.check_presup_trigger()
199            && !self.is_followed_by_np_object()
200            && self.is_followed_by_gerund()
201        {
202            if let Term::Constant(subj_sym) = subject_term {
203                let presup_kind = match self.advance().kind {
204                    TokenType::PresupTrigger(kind) => kind,
205                    TokenType::Verb { lemma, .. } => {
206                        let s = self.interner.resolve(lemma).to_lowercase();
207                        crate::lexicon::lookup_presup_trigger(&s).expect(
208                            "Lexicon mismatch: Verb flagged as trigger but lookup failed",
209                        )
210                    }
211                    _ => unreachable!("guarded by check_presup_trigger"),
212                };
213                let subject_np = crate::ast::NounPhrase::simple(subj_sym);
214                let parsed =
215                    self.parse_presupposition(&subject_np, presup_kind, has_negation)?;
216                if has_modal {
217                    self.drs.exit_box();
218                }
219                if let LogicExpr::Presupposition {
220                    assertion,
221                    presupposition,
222                } = parsed
223                {
224                    let mut asserted: &'a LogicExpr<'a> = assertion;
225                    if has_modal {
226                        if let Some(vector) = modal_vector {
227                            asserted = self.ctx.modal(vector, asserted);
228                        }
229                    }
230                    return Ok(self.ctx.exprs.alloc(LogicExpr::Presupposition {
231                        assertion: asserted,
232                        presupposition,
233                    }));
234                }
235                let mut result: &'a LogicExpr<'a> = parsed;
236                if has_modal {
237                    if let Some(vector) = modal_vector {
238                        result = self.ctx.modal(vector, result);
239                    }
240                }
241                return Ok(result);
242            }
243        }
244
245        let verb = if self.check_verb() {
246            self.consume_verb()
247        } else if self.check_content_word() {
248            self.consume_content_word()?
249        } else {
250            return Err(ParseError {
251                kind: ParseErrorKind::ExpectedContentWord { found: self.peek().kind.clone() },
252                span: self.peek().span.clone(),
253            });
254        };
255
256        let subject_role = if has_passive {
257            ThematicRole::Theme
258        } else {
259            ThematicRole::Agent
260        };
261        let mut roles: Vec<(ThematicRole, Term<'a>)> = vec![(subject_role, subject_term)];
262        let mut object_quant: Option<(QuantifierKind, Symbol, &'a LogicExpr<'a>)> = None;
263        // A DESCRIPTIVE perfect-passive by-agent ("…has been caught by the local
264        // police") becomes its own restrictor-carrying entity scoping the whole
265        // event; captured here, the wrap is applied to the finished `result` below
266        // so the Agent variable it binds stays in scope. Bare agents → None.
267        let mut agent_restr: Option<(Symbol, &'a LogicExpr<'a>)> = None;
268
269        if has_passive && self.check_preposition() {
270            if let TokenType::Preposition(sym) = self.peek().kind {
271                if self.interner.resolve(sym) == "by" {
272                    self.advance();
273                    let agent_np = self.parse_noun_phrase(true)?;
274                    let (agent_term, restr) = self.possessor_entity(&agent_np);
275                    agent_restr = restr;
276                    roles.push((ThematicRole::Agent, agent_term));
277                }
278            }
279        } else if !has_passive && self.counting_np_lookahead().is_some() {
280            // A counting-NP object under a perfect/modal ("has done 49 previous
281            // jumps"): a ∃=n entity that is the Theme, keeping the count AND the
282            // adjectives (the noun-only object_quant path below drops them).
283            let n = self.counting_np_lookahead().unwrap();
284            self.advance(); // the number
285            let var = self.next_var_name();
286            self.nominal_np_context = true;
287            let obj_np_result = self.parse_noun_phrase(false);
288            self.nominal_np_context = false;
289            let obj_np = obj_np_result?;
290            let mut restriction: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::Predicate {
291                name: obj_np.noun,
292                args: self.ctx.terms.alloc_slice([Term::Variable(var)]),
293                world: None,
294            });
295            for &adj in obj_np.adjectives {
296                let adj_pred = self.adjective_restriction(adj, var, obj_np.noun);
297                restriction = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
298                    left: restriction,
299                    op: TokenType::And,
300                    right: adj_pred,
301                });
302            }
303            for pp in obj_np.pps {
304                let pp_sub = self.substitute_pp_placeholder(pp, var);
305                restriction = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
306                    left: restriction,
307                    op: TokenType::And,
308                    right: pp_sub,
309                });
310            }
311            roles.push((ThematicRole::Theme, Term::Variable(var)));
312            object_quant = Some((QuantifierKind::Cardinal(n), var, restriction));
313        } else if !has_passive
314            && matches!(
315                self.peek().kind,
316                TokenType::All | TokenType::Some | TokenType::Cardinal(_)
317            )
318        {
319            // Quantified object under a modal ("shall acknowledge every
320            // request", "shall never drop two consecutive bytes"): the
321            // object raises past the root modal —
322            // ∀x(Request(x) → MODAL(event with Theme x)).
323            let kind = match self.advance().kind {
324                TokenType::All => QuantifierKind::Universal,
325                TokenType::Cardinal(n) => QuantifierKind::Cardinal(n),
326                _ => QuantifierKind::Existential,
327            };
328            let obj_np = self.parse_noun_phrase(false)?;
329            let var = self.next_var_name();
330            roles.push((ThematicRole::Theme, Term::Variable(var)));
331            let restriction = self.ctx.exprs.alloc(LogicExpr::Predicate {
332                name: obj_np.noun,
333                args: self.ctx.terms.alloc_slice([Term::Variable(var)]),
334                world: None,
335            });
336            object_quant = Some((kind, var, restriction));
337        } else if !has_passive && (self.check_content_word() || self.check_article()) {
338            let obj_np = self.parse_noun_phrase(false)?;
339            let obj_term = self.noun_phrase_to_term(&obj_np);
340            roles.push((ThematicRole::Theme, obj_term));
341        } else if !has_passive && self.check_pronoun() {
342            // Pronoun object ("A wolf would eat you."): person deictics resolve
343            // to discourse roles, third person to the discourse referent.
344            let token = self.advance().clone();
345            let plex = self.interner.resolve(token.lexeme).to_lowercase();
346            let obj_term = match plex.as_str() {
347                "you" | "yourself" => Term::Constant(self.interner.intern("Addressee")),
348                "i" | "me" | "myself" => Term::Constant(self.interner.intern("Speaker")),
349                _ => {
350                    let (gender, number) = match &token.kind {
351                        TokenType::Pronoun { gender, number, .. } => (*gender, *number),
352                        _ => (crate::drs::Gender::Unknown, crate::drs::Number::Singular),
353                    };
354                    match self.resolve_pronoun(gender, number)? {
355                        super::ResolvedPronoun::Variable(s) => Term::Variable(s),
356                        super::ResolvedPronoun::Constant(s) => Term::Constant(s),
357                    }
358                }
359            };
360            roles.push((ThematicRole::Theme, obj_term));
361        }
362
363        let event_var = self.get_event_var();
364        let mut modifiers: Vec<Symbol> = Vec::new();
365        // Manner adverbs under the modal ("would spread quickly").
366        modifiers.extend(self.collect_adverbs());
367        if let Some(pending) = self.pending_time {
368            match pending {
369                Time::Past => modifiers.push(self.interner.intern("Past")),
370                Time::Future => modifiers.push(self.interner.intern("Future")),
371                _ => {}
372            }
373        }
374        let suppress_existential = self.drs.in_conditional_antecedent();
375        let base_pred = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
376            event_var,
377            verb,
378            roles: self.ctx.roles.alloc_slice(roles.clone()),
379            modifiers: self.ctx.syms.alloc_slice(modifiers.clone()),
380            suppress_existential,
381            world: None,
382        })));
383
384        // PPs and clause-final particles under the modal ("might walk in",
385        // "would go to the store") modify the same event.
386        let mut base_pred: &'a LogicExpr<'a> = base_pred;
387        while self.check_preposition() {
388            let prep_name = if let TokenType::Preposition(sym) = self.peek().kind {
389                sym
390            } else {
391                break;
392            };
393            // A measure object ("can fly FOR 40 minutes", "runs at 5 mph") — the
394            // amount is the PP's object, kept so the duration/rate isn't dropped.
395            // "with 190 degree WATER" — a measure-premodified noun (head two
396            // tokens past the number, i.e. after the unit) is ONE folded entity,
397            // routed to the NP branch, not a bare measure with the head stranded.
398            let head_after_unit = matches!(
399                self.tokens.get(self.current + 3).map(|t| &t.kind),
400                Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
401            );
402            let measure_follows = matches!(
403                self.tokens.get(self.current + 1).map(|t| &t.kind),
404                Some(TokenType::Number(_) | TokenType::Cardinal(_))
405            ) && !head_after_unit;
406            let np_follows = head_after_unit || match self.tokens.get(self.current + 1).map(|t| &t.kind) {
407                Some(TokenType::Noun(_) | TokenType::ProperName(_) | TokenType::Article(_)) => true,
408                // A noun READING suffices ("during transfer" — "transfer"
409                // lexes Ambiguous{Verb|Noun}); the NP parse commits it.
410                Some(TokenType::Ambiguous { primary, alternatives }) => {
411                    matches!(**primary, TokenType::Noun(_))
412                        || alternatives.iter().any(|t| matches!(t, TokenType::Noun(_)))
413                }
414                _ => false,
415            };
416            let pp_pred = if measure_follows {
417                self.advance(); // preposition
418                let measure = self.parse_measure_phrase()?;
419                self.ctx.exprs.alloc(LogicExpr::Predicate {
420                    name: prep_name,
421                    args: self.ctx.terms.alloc_slice([Term::Variable(event_var), *measure]),
422                    world: None,
423                })
424            } else if np_follows {
425                self.advance(); // preposition
426                let saved_ctx = self.nominal_np_context;
427                self.nominal_np_context = true;
428                let pp_np_result = self.parse_noun_phrase(false);
429                self.nominal_np_context = saved_ctx;
430                let pp_np = pp_np_result?;
431                let pred: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::Predicate {
432                    name: prep_name,
433                    args: self.ctx.terms.alloc_slice([
434                        Term::Variable(event_var),
435                        Term::Constant(pp_np.noun),
436                    ]),
437                    world: None,
438                });
439                // "be caught by the LOCAL police" → By(e, Police) ∧ Local(Police):
440                // the PP object's modifiers survive via the shared helper.
441                self.attach_pp_object_modifiers(pred, &pp_np)
442            } else {
443                self.advance(); // preposition
444                if !self.at_clause_boundary()
445                    || !crate::lexicon::is_particle(
446                        &self.interner.resolve(prep_name).to_lowercase(),
447                    )
448                {
449                    // Not a lexical particle at a clause end — hand it back.
450                    self.current -= 1;
451                    break;
452                }
453                // Intransitive particle/directional: event modifier.
454                self.ctx.exprs.alloc(LogicExpr::Predicate {
455                    name: prep_name,
456                    args: self.ctx.terms.alloc_slice([Term::Variable(event_var)]),
457                    world: None,
458                })
459            };
460            base_pred = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
461                left: base_pred,
462                op: TokenType::And,
463                right: pp_pred,
464            });
465        }
466
467        // Capture template for ellipsis reconstruction
468        self.capture_event_template(verb, &roles, &modifiers);
469
470        let mut result: &'a LogicExpr<'a> = base_pred;
471
472        if has_progressive {
473            result = self.ctx.aspectual(AspectOperator::Progressive, result);
474        }
475
476        if has_passive {
477            result = self.ctx.voice(VoiceOperator::Passive, result);
478        }
479
480        if has_perfect {
481            result = self.ctx.aspectual(AspectOperator::Perfect, result);
482
483            // Check pending_time to set up reference time for tense
484            if let Some(pending) = self.pending_time.take() {
485                match pending {
486                    Time::Future => {
487                        // Future perfect: S < R
488                        let r_var = self.world_state.next_reference_time();
489                        self.world_state.add_time_constraint("S".to_string(), TimeRelation::Precedes, r_var);
490                    }
491                    Time::Past => {
492                        // Past perfect fallback (if not already set by "had")
493                        if self.world_state.current_reference_time() == "S" {
494                            let r_var = self.world_state.next_reference_time();
495                            self.world_state.add_time_constraint(r_var, TimeRelation::Precedes, "S".to_string());
496                        }
497                    }
498                    _ => {}
499                }
500            }
501
502            // Perfect: E < R (event before reference)
503            let e_var = format!("e{}", self.world_state.event_history().len().max(1));
504            let r_var = self.world_state.current_reference_time();
505            self.world_state.add_time_constraint(e_var, TimeRelation::Precedes, r_var);
506        }
507
508        if has_negation {
509            result = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
510                op: TokenType::Not,
511                operand: result,
512            });
513        }
514
515        // Apply nested modal first (from "be able to" = ability)
516        if let Some(vector) = nested_modal_vector {
517            result = self.ctx.modal(vector, result);
518        }
519
520        // Then apply outer modal (e.g., "might")
521        if has_modal {
522            // Exit modal box in DRS (matches enter_box above)
523            self.drs.exit_box();
524            // Note: We do NOT exit_modal_context() here because we want the modal flag
525            // to persist until end_sentence() so telescope candidates are marked as modal.
526            // The modal context is cleared by end_sentence() → prior_modal_context.take()
527            if let Some(vector) = modal_vector {
528                result = self.ctx.modal(vector, result);
529            }
530        }
531
532        if let Some((kind, var, restriction)) = object_quant {
533            let connective = if matches!(kind, QuantifierKind::Universal) {
534                TokenType::Implies
535            } else {
536                TokenType::And
537            };
538            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
539                left: restriction,
540                op: connective,
541                right: result,
542            });
543            result = self.ctx.exprs.alloc(LogicExpr::Quantifier {
544                kind,
545                variable: var,
546                body,
547                island_id: self.current_island,
548            });
549        }
550
551        let result = self.wrap_in_possessor_entity(agent_restr, result);
552        Ok(result)
553    }
554
555    fn token_to_vector(&self, token: &TokenType) -> ModalVector {
556        use crate::ast::ModalFlavor;
557        use super::ModalPreference;
558
559        match token {
560            // Root modals → Narrow Scope (De Re)
561            // These attach the modal to the predicate inside the quantifier
562            TokenType::Must => ModalVector {
563                domain: ModalDomain::Alethic,
564                force: 1.0,
565                flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
566            },
567            TokenType::Cannot => ModalVector {
568                domain: ModalDomain::Alethic,
569                force: 0.0,
570                flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
571            },
572
573            // Polysemous modal: CAN
574            // Default: Ability (Alethic, Root/Narrow)
575            // Deontic: Permission (Deontic, Root/Narrow)
576            TokenType::Can => {
577                match self.modal_preference {
578                    ModalPreference::Deontic => {
579                        // Permission: "You can go" (Deontic, Narrow Scope)
580                        ModalVector {
581                            domain: ModalDomain::Deontic,
582                            force: 0.5,
583                            flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
584                        }
585                    }
586                    _ => {
587                        // Ability: "Birds can fly" (Alethic, Narrow Scope)
588                        ModalVector {
589                            domain: ModalDomain::Alethic,
590                            force: 0.5,
591                            flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
592                        }
593                    }
594                }
595            },
596
597            // Polysemous modal: COULD
598            // Default: Past Ability (Alethic, Root/Narrow)
599            // Epistemic: Conditional Possibility (Alethic, Epistemic/Wide)
600            TokenType::Could => {
601                match self.modal_preference {
602                    ModalPreference::Epistemic => {
603                        // Conditional Possibility: "It could rain" (Alethic, Wide Scope)
604                        ModalVector {
605                            domain: ModalDomain::Alethic,
606                            force: 0.5,
607                            flavor: ModalFlavor::Epistemic, modal_base: None, ordering_source: None
608                        }
609                    }
610                    _ => {
611                        // Past Ability: "She could swim" (Alethic, Narrow Scope)
612                        ModalVector {
613                            domain: ModalDomain::Alethic,
614                            force: 0.5,
615                            flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
616                        }
617                    }
618                }
619            },
620
621            TokenType::Would => ModalVector {
622                domain: ModalDomain::Alethic,
623                force: 0.5,
624                flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
625            },
626            TokenType::Shall => ModalVector {
627                domain: ModalDomain::Deontic,
628                force: 0.9,
629                flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
630            },
631            TokenType::Should => ModalVector {
632                domain: ModalDomain::Deontic,
633                force: 0.6,
634                flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
635            },
636
637            // Epistemic modals → Wide Scope (De Dicto)
638            // These wrap the entire quantifier in the modal
639            TokenType::Might => ModalVector {
640                domain: ModalDomain::Alethic,
641                force: 0.3,
642                flavor: ModalFlavor::Epistemic, modal_base: None, ordering_source: None
643            },
644
645            // Polysemous modal: MAY
646            // Default: Permission (Deontic, Root/Narrow)
647            // Epistemic: Possibility (Alethic, Epistemic/Wide)
648            TokenType::May => {
649                match self.modal_preference {
650                    ModalPreference::Epistemic => {
651                        // Possibility: "It may rain" (Alethic, Wide Scope)
652                        ModalVector {
653                            domain: ModalDomain::Alethic,
654                            force: 0.5,
655                            flavor: ModalFlavor::Epistemic, modal_base: None, ordering_source: None
656                        }
657                    }
658                    _ => {
659                        // Permission: "Students may leave" (Deontic, Narrow Scope)
660                        ModalVector {
661                            domain: ModalDomain::Deontic,
662                            force: 0.5,
663                            flavor: ModalFlavor::Root, modal_base: None, ordering_source: None
664                        }
665                    }
666                }
667            },
668
669            _ => panic!("Unknown modal token: {:?}", token),
670        }
671    }
672}