Skip to main content

logicaffeine_language/parser/
clause.rs

1//! Clause-level parsing: sentences, conditionals, conjunctions, and relative clauses.
2//!
3//! This module handles the top-level sentence structures including:
4//!
5//! - **Simple sentences**: Subject-verb-object patterns
6//! - **Conditionals**: "If P then Q" with DRS scope handling
7//! - **Counterfactuals**: "If P were/had, Q would" (subjunctive)
8//! - **Disjunctions**: "Either P or Q", "P or Q"
9//! - **Conjunctions**: "P and Q"
10//! - **Relative clauses**: "who/that/which" attaching to noun phrases
11//! - **VP ellipsis**: "John ran and Mary did too"
12//!
13//! The [`ClauseParsing`] trait defines the interface implemented by [`Parser`].
14
15use super::modal::ModalParsing;
16use super::noun::NounParsing;
17use super::pragmatics::PragmaticsParsing;
18use super::quantifier::QuantifierParsing;
19use super::question::QuestionParsing;
20use super::verb::LogicVerbParsing;
21use super::{EventTemplate, ParseResult, Parser};
22use crate::ast::{AspectOperator, LogicExpr, NeoEventData, NounPhrase, QuantifierKind, TemporalOperator, Term, ThematicRole};
23use crate::lexer::Lexer;
24use crate::lexicon::Time;
25use crate::drs::{BoxType, Gender, Number};
26use super::ParserMode;
27use crate::error::{ParseError, ParseErrorKind};
28use logicaffeine_base::Symbol;
29use crate::lexicon::Definiteness;
30use crate::token::TokenType;
31
32/// Whether `kind` can BEGIN a clause subject (an NP head/determiner), as opposed to
33/// a copula or verb. Used to tell a fronted temporal adjunct ("Every year SIMON
34/// takes …") from a temporal SUBJECT ("Every year IS long").
35fn starts_clause_subject(kind: &TokenType) -> bool {
36    matches!(
37        kind,
38        TokenType::ProperName(_)
39            | TokenType::Noun(_)
40            | TokenType::Article(_)
41            | TokenType::Pronoun { .. }
42            | TokenType::All
43            | TokenType::No
44            | TokenType::Some
45            | TokenType::Any
46            | TokenType::Most
47            | TokenType::Few
48            | TokenType::Many
49            | TokenType::Cardinal(_)
50            | TokenType::Number(_)
51    )
52}
53
54/// One side of an "Of A and B, …" pair. A bare proper name is a referring
55/// CONSTANT (`is_var == false`); anything with a determiner, adjective,
56/// possessor, PP, or relative clause is a DESCRIPTION carried by a fresh
57/// existential VARIABLE plus a `restrictor` predicate over it — so two NPs that
58/// share a head noun ("the red stamp" / "the blue stamp") stay distinct instead
59/// of collapsing to one constant.
60struct OfEntity<'a> {
61    sym: Symbol,
62    is_var: bool,
63    term: Term<'a>,
64    restrictor: Option<&'a LogicExpr<'a>>,
65}
66
67/// Trait for parsing clause-level structures.
68///
69/// Defines methods for parsing sentences, conditionals, conjunctions,
70/// and other clause-level constructs.
71pub trait ClauseParsing<'a, 'ctx, 'int> {
72    /// Parses a complete sentence, handling imperatives, ellipsis, and questions.
73    fn parse_sentence(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
74    /// Parses "if P then Q" conditionals with DRS scope handling.
75    fn parse_conditional(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
76    /// Parses "either P or Q" exclusive disjunctions.
77    fn parse_either_or(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
78    /// Parses "P or Q" disjunctions.
79    fn parse_disjunction(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
80    /// Parses "P and Q" conjunctions with scope coordination.
81    fn parse_conjunction(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
82    /// Extracts the subject of a copular predication, for non-parallel coordination.
83    fn extract_copular_subject(&self, expr: &'a LogicExpr<'a>) -> Option<Symbol>;
84    /// Parses a bare copular-predicate remnant ("wealthy" / "a philanthropist").
85    fn try_parse_copular_predicate(&mut self, subject: Symbol) -> ParseResult<Option<&'a LogicExpr<'a>>>;
86    /// Parses "who/that/which" relative clauses attaching to noun phrases.
87    fn parse_relative_clause(&mut self, gap_var: Symbol) -> ParseResult<&'a LogicExpr<'a>>;
88    /// Parses a clause with a gap filled by borrowed verb (for VP coordination).
89    fn parse_gapped_clause(&mut self, borrowed_verb: Symbol) -> ParseResult<&'a LogicExpr<'a>>;
90    /// Parses "if P were/had" counterfactual antecedent (subjunctive).
91    fn parse_counterfactual_antecedent(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
92    /// Parses "Q would" counterfactual consequent.
93    fn parse_counterfactual_consequent(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
94    /// Checks if current token is a wh-word (who, what, which, etc.).
95    fn check_wh_word(&self) -> bool;
96    /// Returns true if parsing a counterfactual context.
97    fn is_counterfactual_context(&self) -> bool;
98    /// Returns true if expression is a complete clause.
99    fn is_complete_clause(&self, expr: &LogicExpr<'a>) -> bool;
100    /// Extracts the main verb from an expression.
101    fn extract_verb_from_expr(&self, expr: &LogicExpr<'a>) -> Option<Symbol>;
102    /// Attempts to parse an English imperative ("Close the door.", "Don't touch
103    /// that.", "Let's leave."). Returns `None` (restoring position) when the input
104    /// is not verb-initial / not a hortative or negative command.
105    fn try_parse_imperative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
106    /// True if a finite verb (Verb/Auxiliary/copula/have/modal) appears at or after
107    /// `from`, before the clause terminator. Used to distinguish an imperative
108    /// (command verb is the only finite verb) from a declarative whose initial word
109    /// is a subject ("Set A has cardinality 5.").
110    fn clause_has_later_finite_verb(&self, from: usize) -> bool;
111    /// True when the verb at `vp` heads a reduced object relative (its overt subject
112    /// sits at `vp - 1`, the relativized head is the determiner-headed noun before
113    /// it), so it is NOT the clause's main verb.
114    fn is_reduced_relative_verb(&self, vp: usize) -> bool;
115    /// Attempts to parse an it-cleft "It was X who/that VP." → focus on X plus
116    /// exhaustivity (only X did it). Returns `None` (restoring position) otherwise.
117    fn try_parse_cleft(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
118    /// Attempts to parse an exclamative "How tall she is!" / "What a fool he is!"
119    /// (how/what, no subject-aux inversion, "!"-terminated). Returns `None` otherwise.
120    fn try_parse_exclamative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
121    /// Attempts to parse an optative wish "May you prosper!", "Long live the king!",
122    /// "If only it were Friday!". Returns `None` (restoring position) otherwise.
123    fn try_parse_optative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
124    /// Attempts to parse correlative coordination "Neither X nor Y VP" / "Either X
125    /// or Y VP" — a shared predicate scoped over two subjects. `None` otherwise.
126    fn try_parse_correlative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
127    /// Attempts to parse "Of NP₁ and NP₂, one VP₁ and the other VP₂" →
128    /// (VP₁(NP₁) ∧ VP₂(NP₂)) ∨ (VP₁(NP₂) ∧ VP₂(NP₁)). Returns `None` otherwise.
129    fn try_parse_of_pair_xor(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
130    /// Attempts to parse a sentence-initial temporal NP that FRAMES the clause
131    /// ("Every year Simon takes a holiday" → HAB), not its subject. `None` otherwise.
132    fn try_parse_fronted_temporal_adjunct(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
133    /// Attempts to parse an inverted conditional "Had/Were/Should SUBJECT …, …" by
134    /// un-inverting to the canonical "If SUBJECT aux …" form and reusing the conditional
135    /// parser. Handles multi-word subjects and `Should`-fronting. `None` otherwise.
136    fn try_parse_inverted_conditional(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>>;
137    /// Attempts to parse VP ellipsis ("Mary did too").
138    fn try_parse_ellipsis(&mut self) -> Option<ParseResult<&'a LogicExpr<'a>>>;
139    /// Checks for ellipsis auxiliary (did, does, can, etc.).
140    fn check_ellipsis_auxiliary(&self) -> bool;
141    /// Checks for ellipsis terminator (too, also, as well).
142    fn check_ellipsis_terminator(&self) -> bool;
143}
144
145impl<'a, 'ctx, 'int> ClauseParsing<'a, 'ctx, 'int> for Parser<'a, 'ctx, 'int> {
146    fn try_parse_imperative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
147        let start = self.current;
148        let mut negated = false;
149        // The covert subject of an imperative is the addressee (the hearer); the
150        // hortative "let's" makes it the inclusive group (speaker + addressee).
151        let mut agent_name = "Addressee";
152
153        if self.check(&TokenType::Let) {
154            // Hortative: "Let's leave." / "Let us leave." (Let + 's|us + verb)
155            let n1 = self.current + 1;
156            if n1 >= self.tokens.len() {
157                return Ok(None);
158            }
159            let next = &self.tokens[n1];
160            let next_text = self.interner.resolve(next.lexeme);
161            let is_lets = matches!(next.kind, TokenType::Possessive)
162                || next_text.eq_ignore_ascii_case("us")
163                || next_text.eq_ignore_ascii_case("'s")
164                || next_text.eq_ignore_ascii_case("s");
165            if !is_lets {
166                return Ok(None);
167            }
168            self.advance(); // Let
169            self.advance(); // 's / us
170            agent_name = "Us";
171        } else if self.check(&TokenType::Do) {
172            // Negative imperative: "Don't touch that." → Do + Not + verb
173            let n1 = self.current + 1;
174            if n1 < self.tokens.len() && matches!(self.tokens[n1].kind, TokenType::Not) {
175                self.advance(); // Do
176                self.advance(); // Not
177                negated = true;
178            } else {
179                // "Do you ...?" is a yes/no question, handled elsewhere.
180                return Ok(None);
181            }
182        }
183
184        // Sentence-initial imperatives capitalize the command verb, so the lexer
185        // may have tagged it as a ProperName ("Close" in "Close the door."). Retag
186        // a capitalized known base verb as a Verb — but ONLY when no other finite
187        // verb appears later in the clause. A real imperative has the command verb
188        // as its only finite verb; a later finite verb means the initial capitalized
189        // word is actually a subject ("Bill ran.", "Set A has cardinality 5.").
190        if let TokenType::ProperName(sym) = self.peek().kind {
191            let lemma = self.interner.resolve(sym).to_lowercase();
192            if crate::lexicon::is_base_verb(&lemma)
193                && !self.clause_has_later_finite_verb(self.current + 1)
194            {
195                let class = crate::lexicon::lookup_verb_class(&lemma);
196                self.tokens[self.current].kind = TokenType::Verb {
197                    lemma: sym,
198                    time: Time::Present,
199                    aspect: crate::lexicon::Aspect::Simple,
200                    class,
201                };
202            }
203        }
204
205        // An imperative is verb-initial. English has no bare-verb declaratives, and
206        // yes/no questions begin with an auxiliary (Do/Is/...) rather than a Verb
207        // token, so a Verb in initial position is the command verb.
208        if !self.check_verb() {
209            self.current = start;
210            return Ok(None);
211        }
212
213        // Even when the initial word is a Verb token, a later finite verb means it is
214        // really a subject (e.g. "Set" in "Set A has cardinality 5."), not a command.
215        if self.clause_has_later_finite_verb(self.current + 1) {
216            self.current = start;
217            return Ok(None);
218        }
219
220        let agent = self.interner.intern(agent_name);
221        let core = self.parse_predicate_with_subject(agent)?;
222        let action = if negated {
223            self.ctx.exprs.alloc(LogicExpr::UnaryOp {
224                op: TokenType::Not,
225                operand: core,
226            })
227        } else {
228            core
229        };
230        Ok(Some(self.ctx.exprs.alloc(LogicExpr::Imperative { action })))
231    }
232
233    fn clause_has_later_finite_verb(&self, from: usize) -> bool {
234        let mut j = from;
235        while j < self.tokens.len() {
236            // A finite verb that heads a REDUCED OBJECT RELATIVE ("the friend Simon
237            // WENT with", "the waterfall Derrick PHOTOGRAPHED") is not the clause's
238            // main verb — its presence must NOT veto the imperative reading of a
239            // sentence-initial command verb. The relative's signature is a
240            // determiner-headed noun head followed by a fresh subject (ProperName /
241            // Pronoun) and then this verb. Skip past such a verb (and a trailing
242            // stranded preposition) and keep scanning for a genuine main verb.
243            if matches!(self.tokens[j].kind, TokenType::Verb { .. })
244                && self.is_reduced_relative_verb(j)
245            {
246                let after = j + 1;
247                if matches!(
248                    self.tokens.get(after).map(|t| &t.kind),
249                    Some(TokenType::Preposition(_))
250                ) {
251                    j = after + 1;
252                } else {
253                    j = after;
254                }
255                continue;
256            }
257            match self.tokens[j].kind {
258                TokenType::Period | TokenType::EOF | TokenType::Exclamation => return false,
259                TokenType::Verb { .. }
260                | TokenType::Auxiliary(_)
261                | TokenType::Is
262                | TokenType::Are
263                | TokenType::Was
264                | TokenType::Were
265                | TokenType::Do
266                | TokenType::Does => return true,
267                _ => {
268                    // Some finite verbs (have/has/had, modals) are lexed as other
269                    // token kinds; catch them by lexeme.
270                    let lex = self.interner.resolve(self.tokens[j].lexeme).to_lowercase();
271                    if matches!(
272                        lex.as_str(),
273                        "has" | "have" | "had" | "is" | "are" | "was" | "were"
274                            | "do" | "does" | "did" | "will" | "would" | "can"
275                            | "could" | "should" | "shall" | "may" | "might" | "must"
276                    ) {
277                        return true;
278                    }
279                }
280            }
281            j += 1;
282        }
283        false
284    }
285
286    /// True when the verb at `vp` heads a reduced object relative — i.e. it is the
287    /// finite verb of a relativizer-dropped clause modifying a preceding noun head,
288    /// not a main-clause verb. The relative's overt subject (a ProperName or
289    /// Pronoun) sits at `vp - 1`, and the relativized head is the determiner-headed
290    /// common noun that immediately precedes that subject ("the friend \[Simon\] went",
291    /// "the waterfall \[Derrick\] photographed"). The determiner requirement is what
292    /// distinguishes this from a true main clause whose initial word is a subject
293    /// ("Set A has …" — "A" has no determiner-headed noun before it).
294    fn is_reduced_relative_verb(&self, vp: usize) -> bool {
295        if vp == 0 {
296            return false;
297        }
298        let subj = vp - 1;
299        if !matches!(
300            self.tokens[subj].kind,
301            TokenType::ProperName(_) | TokenType::Pronoun { .. }
302        ) {
303            return false;
304        }
305        if subj == 0 {
306            return false;
307        }
308        // The relativized head must be a common noun (the gap filler).
309        let head = subj - 1;
310        if !matches!(
311            self.tokens[head].kind,
312            TokenType::Noun(_)
313                | TokenType::CalendarUnit(_)
314                | TokenType::Ambiguous { .. }
315        ) {
316            return false;
317        }
318        // Walk back across nouns/adjectives in the head NP to find a determiner —
319        // an article, possessive, or quantifier opening the NP that the relative
320        // modifies. Without one, "X Y verb" is a bare main clause, not a relative.
321        let mut k = head;
322        loop {
323            match self.tokens[k].kind {
324                TokenType::Article(_)
325                | TokenType::Possessive
326                | TokenType::All
327                | TokenType::Some
328                | TokenType::No
329                | TokenType::Any
330                | TokenType::Most
331                | TokenType::Few
332                | TokenType::Many => return true,
333                TokenType::Noun(_)
334                | TokenType::CalendarUnit(_)
335                | TokenType::Adjective(_)
336                | TokenType::Ambiguous { .. } => {
337                    if k == 0 {
338                        return false;
339                    }
340                    k -= 1;
341                }
342                _ => return false,
343            }
344        }
345    }
346
347    fn try_parse_cleft(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
348        let start = self.current;
349        // "It was/is X who/that VP." — the expletive "it" + copula + focus + relative.
350        if !self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("it") {
351            return Ok(None);
352        }
353        if self.current + 1 >= self.tokens.len()
354            || !matches!(self.tokens[self.current + 1].kind, TokenType::Is | TokenType::Was)
355        {
356            return Ok(None);
357        }
358        self.advance(); // it
359        self.advance(); // is/was
360
361        // The focused constituent (a proper name or NP).
362        let focus_np = match self.parse_noun_phrase(false) {
363            Ok(np) => np,
364            Err(_) => {
365                self.current = start;
366                return Ok(None);
367            }
368        };
369        if !self.check(&TokenType::Who) && !self.check(&TokenType::That) {
370            self.current = start;
371            return Ok(None);
372        }
373        self.advance(); // who/that
374
375        let focus_sym = focus_np.noun;
376        // The cleft clause "broke the vase" with the focus as subject — the core
377        // predication.
378        let core = self.parse_predicate_with_subject(focus_sym)?;
379
380        // Exhaustivity: ∀z( core[focus→z] → z = focus ) — no one but the focus did it.
381        let z = self.next_var_name();
382        let core_z = self.substitute_constant_with_var_sym(core, focus_sym, z)?;
383        let identity = self.ctx.exprs.alloc(LogicExpr::Identity {
384            left: self.ctx.terms.alloc(Term::Variable(z)),
385            right: self.ctx.terms.alloc(Term::Constant(focus_sym)),
386        });
387        let implies = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
388            left: core_z,
389            op: TokenType::Implies,
390            right: identity,
391        });
392        let exhaustivity = self.ctx.exprs.alloc(LogicExpr::Quantifier {
393            kind: QuantifierKind::Universal,
394            variable: z,
395            body: implies,
396            island_id: self.current_island,
397        });
398
399        // Focus marker over the core, conjoined with the exhaustivity claim.
400        let focused_term = self.ctx.terms.alloc(Term::Constant(focus_sym));
401        let focus_expr = self.ctx.exprs.alloc(LogicExpr::Focus {
402            kind: crate::token::FocusKind::Cleft,
403            focused: focused_term,
404            scope: core,
405        });
406        Ok(Some(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
407            left: focus_expr,
408            op: TokenType::And,
409            right: exhaustivity,
410        })))
411    }
412
413    fn try_parse_exclamative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
414        let start = self.current;
415        let lead = self.interner.resolve(self.peek().lexeme).to_lowercase();
416        if lead != "how" && lead != "what" {
417            return Ok(None);
418        }
419        // An exclamative is "!"-terminated and has NO subject-aux inversion. A
420        // wh-question ("How tall is she?") inverts and ends with "?"; require "!".
421        let ends_with_exclamation = self.tokens[start..]
422            .iter()
423            .take_while(|t| !matches!(t.kind, TokenType::EOF))
424            .any(|t| matches!(t.kind, TokenType::Exclamation));
425        if !ends_with_exclamation {
426            return Ok(None);
427        }
428        let is_what = lead == "what";
429        self.advance(); // How / What
430        // optional "a"/"an"
431        if self.check_article() {
432            self.advance();
433        }
434        // The gradable adjective (How) or the noun (What).
435        let pred_sym = match self.consume_content_word() {
436            Ok(s) => s,
437            Err(_) => {
438                self.current = start;
439                return Ok(None);
440            }
441        };
442        // The subject (a pronoun or proper name).
443        let subj_sym = if let TokenType::ProperName(s) = self.peek().kind {
444            self.advance();
445            s
446        } else if self.check_pronoun() {
447            let lx = self.interner.resolve(self.peek().lexeme).to_string();
448            self.advance();
449            let cap = lx
450                .chars()
451                .next()
452                .map(|c| c.to_uppercase().collect::<String>() + &lx[1..])
453                .unwrap_or(lx);
454            self.interner.intern(&cap)
455        } else {
456            match self.consume_content_word() {
457                Ok(s) => s,
458                Err(_) => {
459                    self.current = start;
460                    return Ok(None);
461                }
462            }
463        };
464        // optional copula + "!"
465        if matches!(
466            self.peek().kind,
467            TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
468        ) {
469            self.advance();
470        }
471        if self.check(&TokenType::Exclamation) {
472            self.advance();
473        }
474
475        let degree_var = self.next_var_name();
476        // "How tall she is!" → Tall(she, d); "What a fool he is!" → Fool(he).
477        let body = if is_what {
478            self.ctx.exprs.alloc(LogicExpr::Predicate {
479                name: pred_sym,
480                args: self.ctx.terms.alloc_slice([Term::Constant(subj_sym)]),
481                world: None,
482            })
483        } else {
484            self.ctx.exprs.alloc(LogicExpr::Predicate {
485                name: pred_sym,
486                args: self
487                    .ctx
488                    .terms
489                    .alloc_slice([Term::Constant(subj_sym), Term::Variable(degree_var)]),
490                world: None,
491            })
492        };
493        Ok(Some(self.ctx.exprs.alloc(LogicExpr::Exclamative { degree_var, body })))
494    }
495
496    fn try_parse_optative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
497        let start = self.current;
498        // Optatives are "!"-terminated wishes.
499        let ends_with_exclamation = self.tokens[start..]
500            .iter()
501            .take_while(|t| !matches!(t.kind, TokenType::EOF))
502            .any(|t| matches!(t.kind, TokenType::Exclamation));
503        if !ends_with_exclamation {
504            return Ok(None);
505        }
506
507        // "May SUBJ VP!" — may-fronting (a wish, not the deontic modal). "May"
508        // collides with the month proper-name; in some contexts (e.g. theorem
509        // premises) the lexer emits it as a `ProperName("May")`, so accept that
510        // spelling too — the `!` terminator and the SUBJ-VP shape below keep a
511        // genuine month reading ("May 3 is a holiday.") from matching.
512        let is_may_fronting = self.check(&TokenType::May)
513            || matches!(self.peek().kind, TokenType::ProperName(_))
514                && self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("may");
515        if is_may_fronting {
516            self.advance(); // May
517            let subj_sym = if let TokenType::ProperName(s) = self.peek().kind {
518                self.advance();
519                s
520            } else if self.check_pronoun() {
521                let lx = self.interner.resolve(self.peek().lexeme).to_lowercase();
522                self.advance();
523                match lx.as_str() {
524                    "you" => self.interner.intern("Addressee"),
525                    "i" | "me" => self.interner.intern("Speaker"),
526                    other => self.interner.intern(
527                        &(other.chars().next().map(|c| c.to_uppercase().collect::<String>() + &other[1..]).unwrap_or_default()),
528                    ),
529                }
530            } else {
531                match self.parse_noun_phrase(false) {
532                    Ok(np) => np.noun,
533                    Err(_) => {
534                        self.current = start;
535                        return Ok(None);
536                    }
537                }
538            };
539            // The wish verb may be a rare/unlisted word ("prosper"), so capture it
540            // by lexeme rather than requiring a recognized verb token.
541            if self.is_at_end() || self.check(&TokenType::Exclamation) {
542                self.current = start;
543                return Ok(None);
544            }
545            let vlex = self.interner.resolve(self.peek().lexeme).to_string();
546            let vname = vlex
547                .chars()
548                .next()
549                .map(|c| c.to_uppercase().collect::<String>() + &vlex[1..])
550                .unwrap_or(vlex);
551            let verb_sym = self.interner.intern(&vname);
552            self.advance(); // consume the wish verb
553            // Optional object (a pronoun / proper name).
554            let wish = if let TokenType::ProperName(o) = self.peek().kind {
555                self.advance();
556                self.ctx.exprs.alloc(LogicExpr::Predicate {
557                    name: verb_sym,
558                    args: self
559                        .ctx
560                        .terms
561                        .alloc_slice([Term::Constant(subj_sym), Term::Constant(o)]),
562                    world: None,
563                })
564            } else {
565                self.ctx.exprs.alloc(LogicExpr::Predicate {
566                    name: verb_sym,
567                    args: self.ctx.terms.alloc_slice([Term::Constant(subj_sym)]),
568                    world: None,
569                })
570            };
571            return Ok(Some(self.ctx.exprs.alloc(LogicExpr::Optative { wish })));
572        }
573
574        // "Long live NP!" — fixed optative construction.
575        let lead = self.interner.resolve(self.peek().lexeme).to_lowercase();
576        if lead == "long"
577            && self.current + 1 < self.tokens.len()
578            && self.interner.resolve(self.tokens[self.current + 1].lexeme).eq_ignore_ascii_case("live")
579        {
580            self.advance(); // Long
581            self.advance(); // live
582            let np = self.parse_noun_phrase(false)?;
583            let live_sym = self.interner.intern("Live");
584            let wish = self.ctx.exprs.alloc(LogicExpr::Predicate {
585                name: live_sym,
586                args: self.ctx.terms.alloc_slice([Term::Constant(np.noun)]),
587                world: None,
588            });
589            return Ok(Some(self.ctx.exprs.alloc(LogicExpr::Optative { wish })));
590        }
591
592        // "If only S!" — counterfactual wish.
593        if self.check(&TokenType::If)
594            && self.current + 1 < self.tokens.len()
595            && self.interner.resolve(self.tokens[self.current + 1].lexeme).eq_ignore_ascii_case("only")
596        {
597            self.advance(); // If
598            self.advance(); // only
599            let wish = self.parse_sentence()?;
600            return Ok(Some(self.ctx.exprs.alloc(LogicExpr::Optative { wish })));
601        }
602
603        Ok(None)
604    }
605
606    fn try_parse_correlative(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
607        let start = self.current;
608        let lead = self.interner.resolve(self.peek().lexeme).to_lowercase();
609        let is_neither = lead == "neither";
610        let is_either = lead == "either";
611        if !is_neither && !is_either {
612            return Ok(None);
613        }
614        self.advance(); // Neither / Either
615
616        // Parse each disjunct as a FULL noun phrase so multi-word proper names
617        // ("Belle Grove"), possessives ("Pam's client"), and descriptive NPs with
618        // PPs / relative clauses ("the person who paid $150") are preserved with
619        // ZERO meaning loss. A bare proper name OR a bare definite (head only,
620        // nothing to lose) stays a referring CONSTANT; a description carrying
621        // RESTRICTIONS (adjectives / possessor / PPs / relative clause) becomes a
622        // fresh existential VARIABLE with a restrictor, so the shared predicate
623        // distributes over the right entity.
624        fn build_disjunct<'a, 'ctx, 'int>(
625            p: &mut Parser<'a, 'ctx, 'int>,
626        ) -> ParseResult<OfEntity<'a>> {
627            let np = p.parse_noun_phrase(true)?;
628            let has_rel = p.check(&TokenType::Who)
629                || p.check(&TokenType::That)
630                || p.check(&TokenType::Where)
631                || p.check(&TokenType::Whose);
632            let is_desc = !np.adjectives.is_empty()
633                || np.possessor.is_some()
634                || !np.pps.is_empty()
635                || has_rel;
636            let (sym, is_var) = if is_desc {
637                (p.next_var_name(), true)
638            } else {
639                (np.noun, false)
640            };
641            let term = if is_var {
642                Term::Variable(sym)
643            } else {
644                Term::Constant(sym)
645            };
646            let rel = p.try_attach_relative(term)?;
647            let restrictor = if is_var {
648                let mut r = p.nominal_predication(term, &np);
649                for pp in np.pps {
650                    let pp_sub = p.substitute_pp_placeholder(pp, sym);
651                    r = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
652                        left: r,
653                        op: TokenType::And,
654                        right: pp_sub,
655                    });
656                }
657                if let Some(rc) = rel {
658                    r = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
659                        left: r,
660                        op: TokenType::And,
661                        right: rc,
662                    });
663                }
664                Some(r)
665            } else {
666                None
667            };
668            Ok(OfEntity { sym, is_var, term, restrictor })
669        }
670
671        // A disjunct's predicate becomes its branch: a description asserts its
672        // restrictor and binds the predicate under a fresh existential; a bare
673        // constant predicates directly. (For a constant this is exactly the old
674        // ¬pred / pred form, so proper-name correlatives are byte-identical.)
675        fn wrap_branch<'a, 'ctx, 'int>(
676            p: &mut Parser<'a, 'ctx, 'int>,
677            e: &OfEntity<'a>,
678            body: &'a LogicExpr<'a>,
679        ) -> &'a LogicExpr<'a> {
680            if !e.is_var {
681                return body;
682            }
683            let inner = match e.restrictor {
684                Some(r) => p.ctx.exprs.alloc(LogicExpr::BinaryOp {
685                    left: r,
686                    op: TokenType::And,
687                    right: body,
688                }),
689                None => body,
690            };
691            p.ctx.exprs.alloc(LogicExpr::Quantifier {
692                kind: QuantifierKind::Existential,
693                variable: e.sym,
694                body: inner,
695                island_id: p.current_island,
696            })
697        }
698
699        let e1 = match self.try_parse(|p| build_disjunct(p)) {
700            Some(e) => e,
701            None => {
702                self.current = start;
703                return Ok(None);
704            }
705        };
706
707        let conj = self.interner.resolve(self.peek().lexeme).to_lowercase();
708        if conj != "nor" && conj != "or" {
709            self.current = start;
710            return Ok(None);
711        }
712        self.advance(); // nor / or
713
714        let e2 = match self.try_parse(|p| build_disjunct(p)) {
715            Some(e) => e,
716            None => {
717                self.current = start;
718                return Ok(None);
719            }
720        };
721
722        // The shared predicate is parsed once per subject by re-parsing from the
723        // same position (parallel structure), so "Neither X nor Y VP" distributes VP.
724        let vp_start = self.current;
725        let pred1 = if e1.is_var {
726            self.parse_predicate_with_subject_as_var(e1.sym)?
727        } else {
728            self.parse_predicate_with_subject(e1.sym)?
729        };
730        self.current = vp_start;
731        let pred2 = if e2.is_var {
732            self.parse_predicate_with_subject_as_var(e2.sym)?
733        } else {
734            self.parse_predicate_with_subject(e2.sym)?
735        };
736
737        let result = if is_neither {
738            // ¬pred1 ∧ ¬pred2 — a description binds its (asserted) restrictor and
739            // the negated predicate under its own existential.
740            let n1 = self.ctx.exprs.alloc(LogicExpr::UnaryOp { op: TokenType::Not, operand: pred1 });
741            let b1 = wrap_branch(self, &e1, n1);
742            let n2 = self.ctx.exprs.alloc(LogicExpr::UnaryOp { op: TokenType::Not, operand: pred2 });
743            let b2 = wrap_branch(self, &e2, n2);
744            self.ctx.exprs.alloc(LogicExpr::BinaryOp { left: b1, op: TokenType::And, right: b2 })
745        } else {
746            // "either…or" is the inclusive disjunction by default (so the proof engine
747            // and existing tests see a plain ∨); its EXCLUSIVITY implicature
748            // `∧ ¬(branch1 ∧ branch2)` is a pragmatic enrichment, added only in that mode.
749            let b1 = wrap_branch(self, &e1, pred1);
750            let b2 = wrap_branch(self, &e2, pred2);
751            let disj = self.ctx.exprs.alloc(LogicExpr::BinaryOp { left: b1, op: TokenType::Or, right: b2 });
752            if self.pragmatic {
753                let both = self.ctx.exprs.alloc(LogicExpr::BinaryOp { left: b1, op: TokenType::And, right: b2 });
754                let not_both = self.ctx.exprs.alloc(LogicExpr::UnaryOp { op: TokenType::Not, operand: both });
755                self.ctx.exprs.alloc(LogicExpr::BinaryOp { left: disj, op: TokenType::And, right: not_both })
756            } else {
757                disj
758            }
759        };
760        Ok(Some(result))
761    }
762
763    fn try_parse_inverted_conditional(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
764        // A fronted auxiliary (Had / Were / Should) stands in for "if".
765        if !matches!(
766            self.peek().kind,
767            TokenType::Had | TokenType::Were | TokenType::Should
768        ) {
769            return Ok(None);
770        }
771
772        // Require "antecedent, consequent" — a comma before the clause terminator — so an
773        // inverted yes/no question ("Had you eaten?") is not mistaken for a conditional.
774        let has_comma = self.tokens[self.current..]
775            .iter()
776            .take_while(|t| !matches!(t.kind, TokenType::EOF | TokenType::Period))
777            .any(|t| matches!(t.kind, TokenType::Comma));
778        if !has_comma {
779            return Ok(None);
780        }
781
782        // Where the fronted aux un-inverts to: before the antecedent's first verb when
783        // there is one ("Had the soldiers KNOWN" → "the soldiers had known"), else after
784        // the subject-NP head (copular "Were I rich" → "I were rich"). Scanning stops at
785        // the clause comma so the aux never lands in the consequent.
786        let start = self.current + 1;
787        let comma_at = self.tokens[start..]
788            .iter()
789            .position(|t| matches!(t.kind, TokenType::Comma))
790            .map(|p| start + p)
791            .unwrap_or(self.tokens.len());
792
793        let first_verb = (start..comma_at)
794            .find(|&j| matches!(self.tokens[j].kind, TokenType::Verb { .. }));
795
796        // Subject-NP head: optional determiners/adjectives then a nominal head (or, after
797        // a determiner, any content word — "soldiers" is verb/noun-ambiguous here).
798        let mut i = start;
799        while i < comma_at
800            && matches!(
801                self.tokens[i].kind,
802                TokenType::Article(_)
803                    | TokenType::Adjective(_)
804                    | TokenType::Cardinal(_)
805                    | TokenType::Possessive
806            )
807        {
808            i += 1;
809        }
810        let head_is_nominal = i < comma_at
811            && matches!(
812                self.tokens[i].kind,
813                TokenType::Noun(_) | TokenType::ProperName(_) | TokenType::Pronoun { .. }
814            );
815        let head_after_determiner =
816            i > start && i < comma_at && Self::is_content_word_type(&self.tokens[i].kind);
817
818        let insert_at = match first_verb {
819            Some(v) => v,
820            None if head_is_nominal || head_after_determiner => i + 1,
821            None => return Ok(None), // no subject NP / no predicate — not a conditional
822        };
823
824        // Un-invert to canonical order: lift the fronted aux to just after the subject NP
825        // and prepend a synthesized "If", then reuse the conditional parser. Reusing it
826        // keeps one antecedent grammar (weather verbs, conjunction, counterfactual
827        // detection) rather than duplicating it for the inverted word order.
828        let aux = self.tokens.remove(self.current);
829        self.tokens.insert(insert_at - 1, aux);
830        let mut if_tok = self.tokens[self.current].clone();
831        if_tok.kind = TokenType::If;
832        if_tok.lexeme = self.interner.intern("if");
833        self.tokens.insert(self.current, if_tok);
834        self.advance(); // consume the synthesized "If"
835        Ok(Some(self.parse_conditional()?))
836    }
837
838    /// A sentence-initial temporal NP that FRAMES the clause rather than serving as
839    /// its subject: "Every year Simon takes a holiday" → HAB over the whole clause.
840    /// Fires only for "Every/All <calendar-unit>" FOLLOWED BY a clause subject; a
841    /// time NP that is itself the subject ("Every year is long") has a copula/verb
842    /// next and is left to the ordinary quantifier path.
843    fn try_parse_fronted_temporal_adjunct(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
844        let is_universal_det = matches!(self.peek().kind, TokenType::All);
845        let unit_next = matches!(
846            self.tokens.get(self.current + 1).map(|t| &t.kind),
847            Some(TokenType::CalendarUnit(_))
848        );
849        let subject_after = self
850            .tokens
851            .get(self.current + 2)
852            .map_or(false, |t| starts_clause_subject(&t.kind));
853        if !(is_universal_det && unit_next && subject_after) {
854            return Ok(None);
855        }
856        self.advance(); // Every / All
857        self.advance(); // <calendar-unit>
858        if self.check(&TokenType::Comma) {
859            self.advance();
860        }
861        let clause = self.parse_sentence()?;
862        // A present-tense clause ("Simon takes …") already carries HAB; the fronted
863        // "every <unit>" then adds nothing — don't double-wrap.
864        if matches!(clause, LogicExpr::Aspectual { operator: AspectOperator::Habitual, .. }) {
865            return Ok(Some(clause));
866        }
867        Ok(Some(self.ctx.exprs.alloc(LogicExpr::Aspectual {
868            operator: AspectOperator::Habitual,
869            body: clause,
870        })))
871    }
872
873    fn parse_sentence(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
874        // In imperative mode, handle Let statements by converting to LogicExpr
875        // This supports declarative parser being called after process_block_headers()
876        // Let x is/= value -> returns the value expression (the test just checks parsing succeeds)
877        if self.mode == ParserMode::Imperative && self.check(&TokenType::Let) {
878            self.advance(); // consume "Let"
879            let _var = self.expect_identifier()?;
880            // Accept "is", "be", "=" as assignment operators
881            if self.check(&TokenType::Is) || self.check(&TokenType::Be) || self.check(&TokenType::Equals) || self.check(&TokenType::Identity) || self.check(&TokenType::Assign) {
882                self.advance(); // consume the operator
883            }
884            // Parse the value and return it (test just checks parsing succeeds)
885            return self.parse_disjunction();
886        }
887
888        // Check for ellipsis pattern: "Mary does too." / "Mary can too."
889        if let Some(result) = self.try_parse_ellipsis() {
890            return result;
891        }
892
893        // Optatives: "May you prosper!", "Long live the king!", "If only …!".
894        if self.mode != ParserMode::Imperative {
895            if let Some(opt) = self.try_parse_optative()? {
896                return Ok(opt);
897            }
898        }
899
900        // Correlative coordination: "Neither X nor Y VP" / "Either X or Y VP".
901        if self.mode != ParserMode::Imperative {
902            if let Some(corr) = self.try_parse_correlative()? {
903                return Ok(corr);
904            }
905        }
906
907        // "Of NP₁ and NP₂, one VP₁ and the other VP₂" binary XOR partition.
908        if self.mode != ParserMode::Imperative {
909            if let Some(xor) = self.try_parse_of_pair_xor()? {
910                return Ok(xor);
911            }
912        }
913
914        // Sentence-initial temporal adjunct: "Every year Simon takes a holiday"
915        // (habitual) — the fronted time NP frames the whole clause.
916        if self.mode != ParserMode::Imperative {
917            if let Some(framed) = self.try_parse_fronted_temporal_adjunct()? {
918                return Ok(framed);
919            }
920        }
921
922        // "Whoever VP₁ VP₂" → ∀x(VP₁(x) → VP₂(x)).
923        // "Whoever" is not in the lexicon so it arrives as Noun/ProperName; detect by text.
924        if self.mode != ParserMode::Imperative {
925            let lead_text = self.interner.resolve(self.peek().lexeme).to_lowercase();
926            if lead_text == "whoever" {
927                self.advance(); // consume "whoever"
928                let var = self.next_var_name();
929                let restrictor = self.parse_predicate_with_subject(var)?;
930                let scope = self.parse_predicate_with_subject(var)?;
931                let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
932                    left: restrictor,
933                    op: TokenType::Implies,
934                    right: scope,
935                });
936                return Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
937                    kind: QuantifierKind::Universal,
938                    variable: var,
939                    body,
940                    island_id: self.current_island,
941                }));
942            }
943        }
944
945        // Exclamatives: "How tall she is!" / "What a fool he is!" — before the
946        // wh-question path, since they share how/what but are "!"-terminated.
947        if self.mode != ParserMode::Imperative {
948            if let Some(excl) = self.try_parse_exclamative()? {
949                return Ok(excl);
950            }
951        }
952
953        // it-clefts: "It was John who broke the vase." → focus + exhaustivity.
954        if self.mode != ParserMode::Imperative {
955            if let Some(cleft) = self.try_parse_cleft()? {
956                return Ok(cleft);
957            }
958        }
959
960        // English imperatives: bare-verb-initial commands ("Close the door."),
961        // negatives ("Don't touch that."), and hortatives ("Let's leave."). Only in
962        // declarative (English) mode — code mode has its own verb-initial handling.
963        if self.mode != ParserMode::Imperative {
964            if let Some(imp) = self.try_parse_imperative()? {
965                return Ok(imp);
966            }
967        }
968
969        // "Although/Though X, Y" concessive subordinator: Y holds despite X (a
970        // defeated expectation). → Concessive{ main: Y, concession: X }.
971        if self.check(&TokenType::Although) {
972            self.advance(); // consume "Although"/"Though"
973            let concession = self.parse_sentence()?;
974            if self.check(&TokenType::Comma) {
975                self.advance();
976            }
977            let main = self.parse_sentence()?;
978            return Ok(self.ctx.exprs.alloc(LogicExpr::Concessive { main, concession }));
979        }
980
981        // "While X, Y" as temporal duration subordinator
982        // Duration semantics: Y holds for the entire interval where X is true.
983        // Lowered as implication checked globally: G(X → Y)
984        if self.check(&TokenType::While) {
985            self.advance(); // consume "While"
986            let condition = self.parse_sentence()?;
987            if self.check(&TokenType::Comma) {
988                self.advance();
989            }
990            let consequent = self.parse_sentence()?;
991            return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
992                left: condition,
993                op: TokenType::Implies,
994                right: consequent,
995            }));
996        }
997
998        // "When X, Y" as temporal subordinator (before wh-question check)
999        // Disambiguate: subordinator has comma-separated clauses, question does not
1000        if self.check(&TokenType::When) {
1001            let saved = self.current;
1002            let mut found_comma = false;
1003            for i in (self.current + 1)..self.tokens.len() {
1004                match &self.tokens[i].kind {
1005                    TokenType::Comma => { found_comma = true; break; }
1006                    TokenType::Period => break,
1007                    _ => {}
1008                }
1009            }
1010            if found_comma {
1011                self.advance(); // consume "When"
1012                let condition = self.parse_sentence()?;
1013                if self.check(&TokenType::Comma) {
1014                    self.advance();
1015                }
1016                let consequent = self.parse_sentence()?;
1017                return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1018                    left: condition,
1019                    op: TokenType::Implies,
1020                    right: consequent,
1021                }));
1022            }
1023            self.current = saved;
1024        }
1025
1026        // "Whenever X, Y" → same as "When X, Y"
1027        if self.check_content_word() {
1028            let word = self.interner.resolve(self.peek().lexeme).to_string();
1029            if word == "Whenever" || word == "whenever" {
1030                self.advance(); // consume "Whenever"
1031                let condition = self.parse_sentence()?;
1032                if self.check(&TokenType::Comma) {
1033                    self.advance();
1034                }
1035                let consequent = self.parse_sentence()?;
1036                return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1037                    left: condition,
1038                    op: TokenType::Implies,
1039                    right: consequent,
1040                }));
1041            }
1042        }
1043
1044        if self.check_wh_word() {
1045            return self.parse_wh_question();
1046        }
1047
1048        if self.check(&TokenType::Does)
1049            || self.check(&TokenType::Do)
1050            || self.check(&TokenType::Is)
1051            || self.check(&TokenType::Are)
1052            || self.check(&TokenType::Was)
1053            || self.check(&TokenType::Were)
1054            || self.check(&TokenType::Would)
1055            || self.check(&TokenType::Could)
1056            || self.check(&TokenType::Can)
1057        {
1058            return self.parse_yes_no_question();
1059        }
1060
1061        // Inverted conditional (§4.1): "Had I known, …" / "Were I rich, …" /
1062        // "Should it rain, …" — subject-aux inversion stands in for "if".
1063        if let Some(expr) = self.try_parse_inverted_conditional()? {
1064            return Ok(expr);
1065        }
1066
1067        if self.match_token(&[TokenType::If]) {
1068            return self.parse_conditional();
1069        }
1070
1071        // Handle "Either X or Y" disjunction
1072        // Special case: "Either NP1 or NP2 is/are PRED" should apply PRED to both
1073        if self.match_token(&[TokenType::Either]) {
1074            return self.parse_either_or();
1075        }
1076
1077        if self.check_modal() {
1078            self.advance();
1079            return self.parse_modal();
1080        }
1081
1082        if self.match_token(&[TokenType::Not]) {
1083            self.negative_depth += 1;
1084            let inner = self.parse_sentence()?;
1085            self.negative_depth -= 1;
1086            return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1087                op: TokenType::Not,
1088                operand: inner,
1089            }));
1090        }
1091
1092        // "not both every request is valid and every grant is valid" → ¬(X ∧ Y)
1093        // Only triggers for clausal conjunction: "both" + quantifier/determiner
1094        // NOT for conjoined NP: "both Socrates and Plato are men"
1095        if self.check(&TokenType::Both) {
1096            // Peek at token after "both" — if it's a quantifier, this is clausal
1097            let next_is_clausal = if self.current + 1 < self.tokens.len() {
1098                matches!(self.tokens[self.current + 1].kind,
1099                    TokenType::All | TokenType::No | TokenType::Some | TokenType::Any
1100                    | TokenType::Most | TokenType::Few | TokenType::Many
1101                    | TokenType::Cardinal(_) | TokenType::AtLeast(_) | TokenType::AtMost(_)
1102                    | TokenType::Article(_)
1103                )
1104            } else {
1105                false
1106            };
1107            if next_is_clausal {
1108                self.advance(); // consume "both"
1109                let first = self.parse_atom()?;
1110                if self.check(&TokenType::And) {
1111                    self.advance(); // consume "and"
1112                }
1113                let second = self.parse_atom()?;
1114                return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1115                    left: first,
1116                    op: TokenType::And,
1117                    right: second,
1118                }));
1119            }
1120        }
1121
1122        // Sentence-initial temporal operators for hardware verification:
1123        // "Always, P" → Temporal { Always, P }
1124        // "Eventually, P" → Temporal { Eventually, P }
1125        // "Next, P" → Temporal { Next, P }
1126        // "Never P" → Temporal { Always, ¬P }
1127        {
1128            let temporal_op = match &self.peek().kind {
1129                TokenType::Adverb(sym) | TokenType::ScopalAdverb(sym) | TokenType::TemporalAdverb(sym) => {
1130                    let resolved = self.interner.resolve(*sym).to_string();
1131                    match resolved.as_str() {
1132                        "Always" => Some(crate::ast::logic::TemporalOperator::Always),
1133                        "Eventually" => Some(crate::ast::logic::TemporalOperator::Eventually),
1134                        "Next" => Some(crate::ast::logic::TemporalOperator::Next),
1135                        _ => None,
1136                    }
1137                }
1138                // Handle "next" as an adjective token (common fallback)
1139                TokenType::Adjective(sym) => {
1140                    let resolved = self.interner.resolve(*sym).to_string();
1141                    if resolved == "Next" {
1142                        Some(crate::ast::logic::TemporalOperator::Next)
1143                    } else {
1144                        None
1145                    }
1146                }
1147                _ => None,
1148            };
1149            if let Some(op) = temporal_op {
1150                self.advance(); // consume the token
1151                // Optionally consume comma: "Always, P"
1152                if self.check(&TokenType::Comma) {
1153                    self.advance();
1154                }
1155                let body = self.parse_sentence()?;
1156                return Ok(self.ctx.exprs.alloc(LogicExpr::Temporal {
1157                    operator: op,
1158                    body,
1159                }));
1160            }
1161        }
1162        // "Never P" → G(¬P): Always { Not { P } }
1163        if self.check(&TokenType::Never) {
1164            self.advance(); // consume "Never"
1165            // Optionally consume comma
1166            if self.check(&TokenType::Comma) {
1167                self.advance();
1168            }
1169            let body = self.parse_sentence()?;
1170            let negated = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1171                op: TokenType::Not,
1172                operand: body,
1173            });
1174            return Ok(self.ctx.exprs.alloc(LogicExpr::Temporal {
1175                operator: crate::ast::logic::TemporalOperator::Always,
1176                body: negated,
1177            }));
1178        }
1179
1180        // "After X, Y" → X → Y (temporal sequence)
1181        // "Before X, Y" → Y → X
1182        // Handles both "After reset is deasserted, ..." (full clause)
1183        // and "After request, ..." (bare signal/event noun)
1184        if self.check_preposition_is("after") || self.check_preposition_is("After") {
1185            self.advance(); // consume "after"
1186
1187            // Check for bare noun/signal + comma pattern: "After request, ..."
1188            // Also handle Performative tokens (e.g., "request" when not after determiner)
1189            let is_bare_noun_comma = self.current + 1 < self.tokens.len()
1190                && matches!(self.tokens[self.current + 1].kind, TokenType::Comma)
1191                && (self.check_content_word()
1192                    || matches!(self.peek().kind, TokenType::Performative(_)));
1193            let antecedent = if is_bare_noun_comma {
1194                let noun = match self.advance().kind.clone() {
1195                    TokenType::Performative(s) => s,
1196                    TokenType::Noun(s) | TokenType::Adjective(s) | TokenType::ProperName(s) => s,
1197                    TokenType::Verb { lemma, .. } => lemma,
1198                    _ => return Err(crate::error::ParseError {
1199                        kind: crate::error::ParseErrorKind::ExpectedContentWord { found: self.peek().kind.clone() },
1200                        span: self.current_span(),
1201                    }),
1202                };
1203                self.ctx.exprs.alloc(LogicExpr::Atom(noun))
1204            } else {
1205                self.parse_sentence()?
1206            };
1207
1208            if self.check(&TokenType::Comma) {
1209                self.advance();
1210            }
1211            let consequent = self.parse_sentence()?;
1212            let consequent = self.try_wrap_bounded_delay(consequent);
1213            return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1214                left: antecedent,
1215                op: TokenType::Implies,
1216                right: consequent,
1217            }));
1218        }
1219        if self.check_preposition_is("before") || self.check_preposition_is("Before") {
1220            self.advance(); // consume "before"
1221            let first_clause = self.parse_sentence()?;
1222            if self.check(&TokenType::Comma) {
1223                self.advance();
1224            }
1225            let second_clause = self.parse_sentence()?;
1226            return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1227                left: second_clause,
1228                op: TokenType::Implies,
1229                right: first_clause,
1230            }));
1231        }
1232
1233        self.parse_disjunction()
1234    }
1235
1236    fn check_wh_word(&self) -> bool {
1237        if matches!(
1238            self.peek().kind,
1239            TokenType::Who
1240                | TokenType::What
1241                | TokenType::Where
1242                | TokenType::When
1243                | TokenType::Why
1244        ) {
1245            return true;
1246        }
1247        if self.check_preposition() && self.current + 1 < self.tokens.len() {
1248            matches!(
1249                self.tokens[self.current + 1].kind,
1250                TokenType::Who
1251                    | TokenType::What
1252                    | TokenType::Where
1253                    | TokenType::When
1254                    | TokenType::Why
1255            )
1256        } else {
1257            false
1258        }
1259    }
1260
1261    fn parse_conditional(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
1262        let is_counterfactual = self.is_counterfactual_context();
1263
1264        // Biscuit / relevance conditional (§4.2): "If you WANT tea, the kettle is
1265        // hot." — an "if you <relevance-verb> …" antecedent restricts RELEVANCE, not
1266        // truth; the consequent is asserted unconditionally.
1267        let is_biscuit = self.check_pronoun()
1268            && self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("you")
1269            && self
1270                .tokens
1271                .get(self.current + 1)
1272                .map(|t| {
1273                    crate::lexicon::is_relevance_verb(
1274                        &self.interner.resolve(t.lexeme).to_lowercase(),
1275                    )
1276                })
1277                .unwrap_or(false);
1278
1279        // Enter DRS antecedent box - indefinites here get universal force
1280        self.drs.enter_box(BoxType::ConditionalAntecedent);
1281        let mut antecedent = self.parse_counterfactual_antecedent()?;
1282
1283        // Handle conjunction of clauses in antecedent: "If X is Y and Z is W, ..."
1284        while self.check(&TokenType::And) {
1285            self.advance(); // consume "and"
1286            let second = self.parse_counterfactual_antecedent()?;
1287            antecedent = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1288                left: antecedent,
1289                op: TokenType::And,
1290                right: second,
1291            });
1292        }
1293        self.drs.exit_box();
1294
1295        if self.check(&TokenType::Comma) {
1296            self.advance();
1297        }
1298
1299        if self.check(&TokenType::Then) {
1300            self.advance();
1301        }
1302
1303        // Enter DRS consequent box - can access antecedent referents
1304        self.drs.enter_box(BoxType::ConditionalConsequent);
1305        let mut consequent = self.parse_counterfactual_consequent()?;
1306
1307        // Conjunction of consequent clauses: "…, he would have passed and he
1308        // would have celebrated." A non-clausal "and" (NP coordination left
1309        // unconsumed) rolls back and is left for the caller.
1310        while self.check(&TokenType::And) {
1311            let cp = self.checkpoint();
1312            self.advance(); // consume "and"
1313            match self.parse_counterfactual_consequent() {
1314                Ok(second) => {
1315                    consequent = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1316                        left: consequent,
1317                        op: TokenType::And,
1318                        right: second,
1319                    });
1320                }
1321                Err(_) => {
1322                    self.restore(cp);
1323                    break;
1324                }
1325            }
1326        }
1327
1328        // Trailing temporal operators on the consequent — the conditional's
1329        // consequent path does not route through parse_disjunction, so the
1330        // same handlers apply here: "…, P until Q." / "…, P in the next
1331        // cycle." / "…, P within N cycles."
1332        if self.check(&TokenType::Until)
1333            || self.check(&TokenType::Release)
1334            || self.check(&TokenType::WeakUntil)
1335        {
1336            let op = match self.peek().kind {
1337                TokenType::Release => crate::ast::logic::BinaryTemporalOp::Release,
1338                TokenType::WeakUntil => crate::ast::logic::BinaryTemporalOp::WeakUntil,
1339                _ => crate::ast::logic::BinaryTemporalOp::Until,
1340            };
1341            self.advance();
1342            let right = self.parse_counterfactual_consequent()?;
1343            consequent = self.ctx.exprs.alloc(LogicExpr::TemporalBinary {
1344                operator: op,
1345                left: consequent,
1346                right,
1347            });
1348        }
1349        consequent = self.try_wrap_next_cycle(consequent);
1350        consequent = self.try_wrap_bounded_delay(consequent);
1351        self.drs.exit_box();
1352
1353        // Biscuit conditional: assert the consequent and mark the antecedent as a
1354        // relevance condition — `consequent ∧ Relevance(⟨antecedent⟩)`.
1355        if is_biscuit {
1356            let relevance = self.ctx.exprs.alloc(LogicExpr::Predicate {
1357                name: self.interner.intern("Relevance"),
1358                args: self.ctx.terms.alloc_slice([Term::Proposition(antecedent)]),
1359                world: None,
1360            });
1361            return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1362                left: consequent,
1363                op: TokenType::And,
1364                right: relevance,
1365            }));
1366        }
1367
1368        // Get DRS referents that need universal quantification
1369        let universal_refs = self.drs.get_universal_referents();
1370
1371        // Build the conditional expression
1372        let conditional = if is_counterfactual {
1373            self.ctx.exprs.alloc(LogicExpr::Counterfactual {
1374                antecedent,
1375                consequent,
1376            })
1377        } else {
1378            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1379                left: antecedent,
1380                op: TokenType::If,
1381                right: consequent,
1382            })
1383        };
1384
1385        // Wrap with universal quantifiers for DRS referents
1386        let mut result = conditional;
1387        for var in universal_refs.into_iter().rev() {
1388            result = self.ctx.exprs.alloc(LogicExpr::Quantifier {
1389                kind: QuantifierKind::Universal,
1390                variable: var,
1391                body: result,
1392                island_id: self.current_island,
1393            });
1394        }
1395
1396        Ok(result)
1397    }
1398
1399    /// Parse "Either NP1 or NP2 is/are PRED" or "Either S1 or S2"
1400    ///
1401    /// Handles coordination: "Either Alice or Bob is guilty" should become
1402    /// guilty(Alice) ∨ guilty(Bob), not Alice ∨ guilty(Bob)
1403    fn parse_either_or(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
1404        // Save position for potential backtracking
1405        let start_pos = self.current;
1406
1407        // Try to parse as "Either NP1 or NP2 VP"
1408        // First, try to parse just a proper name (not a full clause)
1409        if let TokenType::ProperName(name1) = self.peek().kind {
1410            self.advance(); // consume first proper name
1411
1412            if self.check(&TokenType::Or) {
1413                self.advance(); // consume "or"
1414
1415                if let TokenType::ProperName(name2) = self.peek().kind {
1416                    self.advance(); // consume second proper name
1417
1418                    // Check for shared predicate: "is/are ADJECTIVE"
1419                    let is_copula = matches!(
1420                        self.peek().kind,
1421                        TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
1422                    );
1423                    if is_copula {
1424                        self.advance(); // consume copula
1425
1426                        // Check for negation: "is not"
1427                        let is_negated = self.match_token(&[TokenType::Not]);
1428
1429                        // Try to get an adjective
1430                        if let TokenType::Adjective(adj) = self.peek().kind {
1431                            self.advance(); // consume adjective
1432
1433                            // Create predicate for each NP
1434                            let pred1 = self.ctx.exprs.alloc(LogicExpr::Predicate {
1435                                name: adj,
1436                                args: self.ctx.terms.alloc_slice(vec![
1437                                    Term::Constant(name1)
1438                                ]),
1439                                world: None,
1440                            });
1441                            let pred2 = self.ctx.exprs.alloc(LogicExpr::Predicate {
1442                                name: adj,
1443                                args: self.ctx.terms.alloc_slice(vec![
1444                                    Term::Constant(name2)
1445                                ]),
1446                                world: None,
1447                            });
1448
1449                            // Apply negation if needed
1450                            let left = if is_negated {
1451                                self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1452                                    op: TokenType::Not,
1453                                    operand: pred1,
1454                                })
1455                            } else {
1456                                pred1
1457                            };
1458                            let right = if is_negated {
1459                                self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1460                                    op: TokenType::Not,
1461                                    operand: pred2,
1462                                })
1463                            } else {
1464                                pred2
1465                            };
1466
1467                            return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1468                                left,
1469                                op: TokenType::Or,
1470                                right,
1471                            }));
1472                        }
1473                    }
1474                }
1475            }
1476
1477            // Backtrack if the special case didn't match
1478            self.current = start_pos;
1479        }
1480
1481        // Fall back to general disjunction parsing
1482        // Enter disjunct box for left side - referents here are inaccessible outward
1483        self.drs.enter_box(BoxType::Disjunct);
1484        let left = self.parse_conjunction()?;
1485        self.drs.exit_box();
1486
1487        if !self.check(&TokenType::Or) {
1488            return Err(ParseError {
1489                kind: ParseErrorKind::ExpectedKeyword { keyword: "or".to_string() },
1490                span: self.current_span(),
1491            });
1492        }
1493        self.advance(); // consume "or"
1494
1495        // Enter disjunct box for right side - referents here are also inaccessible outward
1496        self.drs.enter_box(BoxType::Disjunct);
1497        let right = self.parse_conjunction()?;
1498        self.drs.exit_box();
1499
1500        Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1501            left,
1502            op: TokenType::Or,
1503            right,
1504        }))
1505    }
1506
1507    fn is_counterfactual_context(&self) -> bool {
1508        for i in 0..5 {
1509            if self.current + i >= self.tokens.len() {
1510                break;
1511            }
1512            let token = &self.tokens[self.current + i];
1513            if matches!(token.kind, TokenType::Were | TokenType::Had) {
1514                return true;
1515            }
1516            if matches!(token.kind, TokenType::Comma | TokenType::Period) {
1517                break;
1518            }
1519        }
1520        false
1521    }
1522
1523    fn parse_counterfactual_antecedent(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
1524        let unknown = self.interner.intern("?");
1525        if self.check_content_word() || self.check_pronoun() || self.check_article() {
1526            // Weather verb detection: "if it rains" → ∃e(Rain(e))
1527            // Must check BEFORE pronoun resolution since "it" would resolve to "?"
1528            if self.check_pronoun() {
1529                let token = self.peek();
1530                let token_text = self.interner.resolve(token.lexeme);
1531                if token_text.eq_ignore_ascii_case("it") {
1532                    // Look ahead for weather verb: "it rains" or "it is raining"
1533                    if self.current + 1 < self.tokens.len() {
1534                        // Check for "it + verb" pattern
1535                        if let TokenType::Verb { lemma, time, .. } = &self.tokens[self.current + 1].kind {
1536                            let lemma_str = self.interner.resolve(*lemma);
1537                            if Lexer::is_weather_verb(lemma_str) {
1538                                let verb = *lemma;
1539                                let verb_time = *time;
1540                                self.advance(); // consume "it"
1541                                self.advance(); // consume weather verb
1542
1543                                let event_var = self.get_event_var();
1544
1545                                // Weather verbs are impersonal - no pronoun resolution needed
1546                                // Event var gets universal force from transpiler when suppress_existential=true
1547                                let suppress_existential = self.drs.in_conditional_antecedent();
1548
1549                                let mut result: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1550                                    event_var,
1551                                    verb,
1552                                    roles: self.ctx.roles.alloc_slice(vec![]),
1553                                    modifiers: self.ctx.syms.alloc_slice(vec![]),
1554                                    suppress_existential,
1555                                    world: None,
1556                                })));
1557
1558                                // Handle coordinated weather verbs: "rains and thunders" or "rains or thunders"
1559                                // SHARE the same event_var for all coordinated verbs
1560                                while self.check(&TokenType::And) || self.check(&TokenType::Or) {
1561                                    let is_disjunction = self.check(&TokenType::Or);
1562                                    self.advance(); // consume "and" or "or"
1563
1564                                    if let TokenType::Verb { lemma: lemma2, .. } = &self.peek().kind.clone() {
1565                                        let lemma2_str = self.interner.resolve(*lemma2);
1566                                        if Lexer::is_weather_verb(lemma2_str) {
1567                                            let verb2 = *lemma2;
1568                                            self.advance(); // consume second weather verb
1569
1570                                            // REUSE same event_var - no new variable, no DRS registration
1571                                            let neo_event2 = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1572                                                event_var,  // Same variable as first weather verb
1573                                                verb: verb2,
1574                                                roles: self.ctx.roles.alloc_slice(vec![]),
1575                                                modifiers: self.ctx.syms.alloc_slice(vec![]),
1576                                                suppress_existential,
1577                                                world: None,
1578                                            })));
1579
1580                                            let op = if is_disjunction { TokenType::Or } else { TokenType::And };
1581                                            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1582                                                left: result,
1583                                                op,
1584                                                right: neo_event2,
1585                                            });
1586                                        } else {
1587                                            break; // Not a weather verb, stop coordination
1588                                        }
1589                                    } else {
1590                                        break;
1591                                    }
1592                                }
1593
1594                                return Ok(match verb_time {
1595                                    Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
1596                                        operator: TemporalOperator::Past,
1597                                        body: result,
1598                                    }),
1599                                    Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
1600                                        operator: TemporalOperator::Future,
1601                                        body: result,
1602                                    }),
1603                                    _ => result,
1604                                });
1605                            }
1606                        }
1607                        // Check for "it + is/are + verb" pattern: "it is raining"
1608                        else if self.current + 2 < self.tokens.len() {
1609                            let is_copula = matches!(
1610                                self.tokens[self.current + 1].kind,
1611                                TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
1612                            );
1613                            if is_copula {
1614                                if let TokenType::Verb { lemma, .. } = &self.tokens[self.current + 2].kind {
1615                                    let lemma_str = self.interner.resolve(*lemma);
1616                                    if Lexer::is_weather_verb(lemma_str) {
1617                                        let verb = *lemma;
1618                                        let verb_time = if matches!(
1619                                            self.tokens[self.current + 1].kind,
1620                                            TokenType::Was | TokenType::Were
1621                                        ) {
1622                                            Time::Past
1623                                        } else {
1624                                            Time::Present
1625                                        };
1626                                        self.advance(); // consume "it"
1627                                        self.advance(); // consume "is/are/was/were"
1628                                        self.advance(); // consume weather verb
1629
1630                                        let event_var = self.get_event_var();
1631                                        // Weather verbs are impersonal - no pronoun resolution needed
1632                                        // Event var gets universal force from transpiler when suppress_existential=true
1633                                        let suppress_existential = self.drs.in_conditional_antecedent();
1634
1635                                        let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1636                                            event_var,
1637                                            verb,
1638                                            roles: self.ctx.roles.alloc_slice(vec![]),
1639                                            modifiers: self.ctx.syms.alloc_slice(vec![]),
1640                                            suppress_existential,
1641                                            world: None,
1642                                        })));
1643
1644                                        // Progressive aspect for "is raining"
1645                                        let with_aspect = self.ctx.exprs.alloc(LogicExpr::Aspectual {
1646                                            operator: AspectOperator::Progressive,
1647                                            body: neo_event,
1648                                        });
1649
1650                                        return Ok(match verb_time {
1651                                            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
1652                                                operator: TemporalOperator::Past,
1653                                                body: with_aspect,
1654                                            }),
1655                                            _ => with_aspect,
1656                                        });
1657                                    }
1658                                }
1659                            }
1660                        }
1661                    }
1662                }
1663            }
1664
1665            // Track if subject is an indefinite that needs DRS registration
1666            let (subject, subject_type_pred) = if self.check_pronoun() {
1667                let token = self.advance().clone();
1668                let token_text = self.interner.resolve(token.lexeme);
1669                // Handle first/second person pronouns as constants (deictic reference)
1670                let resolved = if token_text.eq_ignore_ascii_case("i") {
1671                    self.interner.intern("Speaker")
1672                } else if token_text.eq_ignore_ascii_case("you") {
1673                    self.interner.intern("Addressee")
1674                } else if let TokenType::Pronoun { gender, number, .. } = token.kind {
1675                    let resolved_pronoun = self.resolve_pronoun(gender, number)?;
1676                    match resolved_pronoun {
1677                        super::ResolvedPronoun::Variable(s) | super::ResolvedPronoun::Constant(s) => s,
1678                    }
1679                } else {
1680                    unknown
1681                };
1682                (resolved, None)
1683            } else {
1684                let np = self.parse_noun_phrase(true)?;
1685
1686                // Check if this NP should introduce a DRS referent
1687                // Both indefinites ("a dog") and definites ("the dog") introduce referents
1688                // For definites without antecedent, this implements "global accommodation"
1689                if np.definiteness == Some(Definiteness::Definite)
1690                    || np.definiteness == Some(Definiteness::Distal) {
1691                    // A definite description ("the butler") denotes a UNIQUE
1692                    // individual: like a proper name it is a RIGID constant, and
1693                    // every co-referring pronoun ("…he…") must resolve to that
1694                    // SAME constant. Register the referent as rigid (so anaphora
1695                    // binds to the constant, not a fresh variable) but emit NO
1696                    // type predicate and NO variable subject — a `Variable` here
1697                    // would take universal force in the antecedent and diverge
1698                    // from the constant its pronoun and the goal resolve to,
1699                    // which the kernel certifier cannot reconcile.
1700                    let gender = Self::infer_noun_gender(self.interner.resolve(np.noun));
1701                    let number = if Self::is_plural_noun(self.interner.resolve(np.noun)) {
1702                        Number::Plural
1703                    } else {
1704                        Number::Singular
1705                    };
1706                    self.drs.introduce_referent_with_source(
1707                        np.noun,
1708                        np.noun,
1709                        gender,
1710                        number,
1711                        crate::drs::ReferentSource::ProperName,
1712                    );
1713                    (np.noun, None)
1714                } else if np.definiteness == Some(Definiteness::Indefinite) {
1715                    let gender = Self::infer_noun_gender(self.interner.resolve(np.noun));
1716                    let number = if Self::is_plural_noun(self.interner.resolve(np.noun)) {
1717                        Number::Plural
1718                    } else {
1719                        Number::Singular
1720                    };
1721
1722                    // Register in DRS using noun as variable (for pronoun resolution)
1723                    // INDEFINITES ("a X") use default source (universal force in antecedent)
1724                    self.drs.introduce_referent(np.noun, np.noun, gender, number);
1725
1726                    // Create type predicate: Farmer(noun)
1727                    let type_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1728                        name: np.noun,
1729                        args: self.ctx.terms.alloc_slice([Term::Variable(np.noun)]),
1730                        world: None,
1731                    });
1732
1733                    (np.noun, Some(type_pred))
1734                } else {
1735                    // Proper name - use as constant (proper names have their own registration)
1736                    (np.noun, None)
1737                }
1738            };
1739
1740            // Determine the subject term type
1741            let subject_term = if subject_type_pred.is_some() {
1742                Term::Variable(subject)
1743            } else {
1744                Term::Constant(subject)
1745            };
1746
1747            // Handle presupposition triggers in antecedent: "If John stopped smoking, ..."
1748            // Only trigger if followed by gerund complement
1749            if self.check_presup_trigger() && self.is_followed_by_gerund() {
1750                let presup_kind = match self.advance().kind {
1751                    TokenType::PresupTrigger(kind) => kind,
1752                    TokenType::Verb { lemma, .. } => {
1753                        let s = self.interner.resolve(lemma).to_lowercase();
1754                        crate::lexicon::lookup_presup_trigger(&s)
1755                            .expect("Lexicon mismatch: Verb flagged as trigger but lookup failed")
1756                    }
1757                    _ => panic!("Expected presupposition trigger"),
1758                };
1759                let np = NounPhrase {
1760                    noun: subject,
1761                    definiteness: None,
1762                    adjectives: &[],
1763                    possessor: None,
1764                    pps: &[],
1765                    superlative: None,
1766                };
1767                return self.parse_presupposition(&np, presup_kind, false);
1768            }
1769
1770            if self.check(&TokenType::Were) {
1771                self.advance();
1772                let predicate = if self.check_pronoun() {
1773                    let token = self.advance().clone();
1774                    if let TokenType::Pronoun { gender, number, .. } = token.kind {
1775                        let token_text = self.interner.resolve(token.lexeme);
1776                        if token_text.eq_ignore_ascii_case("i") {
1777                            self.interner.intern("Speaker")
1778                        } else if token_text.eq_ignore_ascii_case("you") {
1779                            self.interner.intern("Addressee")
1780                        } else {
1781                            let resolved_pronoun = self.resolve_pronoun(gender, number)?;
1782                            match resolved_pronoun {
1783                                super::ResolvedPronoun::Variable(s) | super::ResolvedPronoun::Constant(s) => s,
1784                            }
1785                        }
1786                    } else {
1787                        unknown
1788                    }
1789                } else {
1790                    self.consume_content_word()?
1791                };
1792                let be = self.interner.intern("Be");
1793                let be_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1794                    name: be,
1795                    args: self.ctx.terms.alloc_slice([
1796                        subject_term,
1797                        Term::Constant(predicate),
1798                    ]),
1799                    world: None,
1800                });
1801                // Combine with type predicate if indefinite subject
1802                return Ok(if let Some(type_pred) = subject_type_pred {
1803                    self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1804                        left: type_pred,
1805                        op: TokenType::And,
1806                        right: be_pred,
1807                    })
1808                } else {
1809                    be_pred
1810                });
1811            }
1812
1813            if self.check(&TokenType::Had) {
1814                self.advance();
1815                // "If John had NOT studied, …" — negated antecedent.
1816                let negated = self.check(&TokenType::Not);
1817                if negated {
1818                    self.advance();
1819                }
1820                let verb = self.consume_content_word()?;
1821                let mut main_pred: &'a LogicExpr<'a> =
1822                    self.ctx.exprs.alloc(LogicExpr::Predicate {
1823                        name: verb,
1824                        args: self.ctx.terms.alloc_slice([subject_term]),
1825                        world: None,
1826                    });
1827                if negated {
1828                    main_pred = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1829                        op: TokenType::Not,
1830                        operand: main_pred,
1831                    });
1832                }
1833
1834                // Handle "because" causal clause in antecedent
1835                // Phase 35: Do NOT consume if followed by string literal (Trust justification)
1836                if self.check(&TokenType::Because) && !self.peek_next_is_string_literal() {
1837                    self.advance();
1838                    let cause = self.parse_atom()?;
1839                    let causal = self.ctx.exprs.alloc(LogicExpr::Causal {
1840                        effect: main_pred,
1841                        cause,
1842                    });
1843                    // Combine with type predicate if indefinite subject
1844                    return Ok(if let Some(type_pred) = subject_type_pred {
1845                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1846                            left: type_pred,
1847                            op: TokenType::And,
1848                            right: causal,
1849                        })
1850                    } else {
1851                        causal
1852                    });
1853                }
1854
1855                // Combine with type predicate if indefinite subject
1856                return Ok(if let Some(type_pred) = subject_type_pred {
1857                    self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1858                        left: type_pred,
1859                        op: TokenType::And,
1860                        right: main_pred,
1861                    })
1862                } else {
1863                    main_pred
1864                });
1865            }
1866
1867            // Parse verb phrase with subject
1868            // Use variable term for indefinite subjects, constant for definites/proper names
1869            let verb_phrase = if subject_type_pred.is_some() {
1870                self.parse_predicate_with_subject_as_var(subject)?
1871            } else {
1872                self.parse_predicate_with_subject(subject)?
1873            };
1874
1875            // Combine with type predicate if indefinite subject
1876            return Ok(if let Some(type_pred) = subject_type_pred {
1877                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1878                    left: type_pred,
1879                    op: TokenType::And,
1880                    right: verb_phrase,
1881                })
1882            } else {
1883                verb_phrase
1884            });
1885        }
1886
1887        self.parse_sentence()
1888    }
1889
1890    fn parse_counterfactual_consequent(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
1891        let unknown = self.interner.intern("?");
1892        if self.check_content_word() || self.check_pronoun() {
1893            // Check for grammatically incorrect "its" + weather adjective
1894            // "its" is possessive, "it's" is contraction - common typo
1895            if self.check_pronoun() {
1896                let token = self.peek();
1897                let token_text = self.interner.resolve(token.lexeme).to_lowercase();
1898                if token_text == "its" {
1899                    // Check if followed by weather adjective
1900                    if self.current + 1 < self.tokens.len() {
1901                        let next_token = &self.tokens[self.current + 1];
1902                        let next_str = self.interner.resolve(next_token.lexeme).to_lowercase();
1903                        if let Some(meta) = crate::lexicon::lookup_adjective_db(&next_str) {
1904                            if meta.features.contains(&crate::lexicon::Feature::Weather) {
1905                                return Err(ParseError {
1906                                    kind: ParseErrorKind::GrammarError(
1907                                        "Did you mean 'it's' (it is)? 'its' is a possessive pronoun.".to_string()
1908                                    ),
1909                                    span: self.current_span(),
1910                                });
1911                            }
1912                        }
1913                    }
1914                }
1915            }
1916
1917            // Check for expletive "it" + copula + weather adjective: "it's wet" → Wet
1918            if self.check_pronoun() {
1919                let token_text = self.interner.resolve(self.peek().lexeme).to_lowercase();
1920                if token_text == "it" {
1921                    // Look ahead for copula + weather adjective
1922                    // Handle both "it is wet" and "it's wet" (where 's is Possessive token)
1923                    if self.current + 2 < self.tokens.len() {
1924                        let next = &self.tokens[self.current + 1].kind;
1925                        if matches!(next, TokenType::Is | TokenType::Was | TokenType::Possessive) {
1926                            // Check if followed by weather adjective
1927                            let adj_token = &self.tokens[self.current + 2];
1928                            let adj_sym = adj_token.lexeme;
1929                            let adj_str = self.interner.resolve(adj_sym).to_lowercase();
1930                            if let Some(meta) = crate::lexicon::lookup_adjective_db(&adj_str) {
1931                                if meta.features.contains(&crate::lexicon::Feature::Weather) {
1932                                    self.advance(); // consume "it"
1933                                    self.advance(); // consume copula
1934                                    self.advance(); // consume adjective token
1935
1936                                    // Use the canonical lemma from lexicon (e.g., "Wet" not "wet")
1937                                    let adj_lemma = self.interner.intern(meta.lemma);
1938
1939                                    // Get event variable from DRS (introduced in antecedent)
1940                                    let event_var = self.drs.get_last_event_referent(self.interner)
1941                                        .unwrap_or_else(|| self.interner.intern("e"));
1942
1943                                    // First weather adjective predicate
1944                                    let mut result: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::Predicate {
1945                                        name: adj_lemma,
1946                                        args: self.ctx.terms.alloc_slice([Term::Variable(event_var)]),
1947                                        world: None,
1948                                    });
1949
1950                                    // Handle coordinated adjectives: "wet and cold"
1951                                    while self.check(&TokenType::And) {
1952                                        self.advance(); // consume "and"
1953                                        if self.check_content_word() {
1954                                            let adj2_lexeme = self.peek().lexeme;
1955                                            let adj2_str = self.interner.resolve(adj2_lexeme).to_lowercase();
1956
1957                                            // Check if it's also a weather adjective
1958                                            if let Some(meta2) = crate::lexicon::lookup_adjective_db(&adj2_str) {
1959                                                if meta2.features.contains(&crate::lexicon::Feature::Weather) {
1960                                                    self.advance(); // consume adjective token
1961                                                    // Use the canonical lemma from lexicon (e.g., "Cold" not "cold")
1962                                                    let adj2_lemma = self.interner.intern(meta2.lemma);
1963                                                    let pred2 = self.ctx.exprs.alloc(LogicExpr::Predicate {
1964                                                        name: adj2_lemma,
1965                                                        args: self.ctx.terms.alloc_slice([Term::Variable(event_var)]),
1966                                                        world: None,
1967                                                    });
1968                                                    result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1969                                                        left: result,
1970                                                        op: TokenType::And,
1971                                                        right: pred2,
1972                                                    });
1973                                                    continue;
1974                                                }
1975                                            }
1976                                        }
1977                                        break;
1978                                    }
1979
1980                                    return Ok(result);
1981                                }
1982                            }
1983                        }
1984                    }
1985                }
1986            }
1987
1988            let subject = if self.check_pronoun() {
1989                let token = self.advance().clone();
1990                let token_text = self.interner.resolve(token.lexeme);
1991                // Handle first/second person pronouns as constants (deictic reference)
1992                if token_text.eq_ignore_ascii_case("i") {
1993                    self.interner.intern("Speaker")
1994                } else if token_text.eq_ignore_ascii_case("you") {
1995                    self.interner.intern("Addressee")
1996                } else if let TokenType::Pronoun { gender, number, .. } = token.kind {
1997                    let resolved_pronoun = self.resolve_pronoun(gender, number)?;
1998                    match resolved_pronoun {
1999                        super::ResolvedPronoun::Variable(s) | super::ResolvedPronoun::Constant(s) => s,
2000                    }
2001                } else {
2002                    unknown
2003                }
2004            } else {
2005                let np = self.parse_noun_phrase(true)?;
2006                if np.definiteness == Some(crate::lexicon::Definiteness::Definite) {
2007                    // A definite presupposes existence: accommodate the
2008                    // referent GLOBALLY (highest box) so later mentions BIND
2009                    // to it ("…, the kettle is hot." then "The kettle is
2010                    // hot." reuses the same individual).
2011                    self.drs.introduce_referent_global(
2012                        np.noun,
2013                        np.noun,
2014                        Gender::Unknown,
2015                        Number::Singular,
2016                        crate::drs::ReferentSource::MainClause,
2017                    );
2018                }
2019                np.noun
2020            };
2021
2022            if self.check(&TokenType::Would) {
2023                self.advance();
2024                // "…, he would NOT have failed." — negated consequent.
2025                let negated = self.check(&TokenType::Not);
2026                if negated {
2027                    self.advance();
2028                }
2029                if self.check_content_word() {
2030                    let next_word = self.interner.resolve(self.peek().lexeme).to_lowercase();
2031                    if next_word == "have" {
2032                        self.advance();
2033                    }
2034                }
2035                // A bare verb keeps the simple predication shape; anything
2036                // after it ("would buy a boat") takes the full VP grammar.
2037                let clause_ends_after_verb = matches!(
2038                    self.tokens.get(self.current + 1).map(|t| t.kind.clone()),
2039                    Some(
2040                        TokenType::Period
2041                            | TokenType::Exclamation
2042                            | TokenType::EOF
2043                            | TokenType::And
2044                            | TokenType::Comma
2045                    ) | None
2046                );
2047                let mut pred: &'a LogicExpr<'a> = if clause_ends_after_verb {
2048                    let verb = self.consume_content_word()?;
2049                    self.ctx.exprs.alloc(LogicExpr::Predicate {
2050                        name: verb,
2051                        args: self.ctx.terms.alloc_slice([Term::Constant(subject)]),
2052                        world: None,
2053                    })
2054                } else {
2055                    self.parse_predicate_with_subject(subject)?
2056                };
2057                if negated {
2058                    pred = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2059                        op: TokenType::Not,
2060                        operand: pred,
2061                    });
2062                }
2063                return Ok(pred);
2064            }
2065
2066            return self.parse_predicate_with_subject(subject);
2067        }
2068
2069        self.parse_sentence()
2070    }
2071
2072    fn extract_verb_from_expr(&self, expr: &LogicExpr<'a>) -> Option<Symbol> {
2073        match expr {
2074            // NeoEvent directly contains the verb
2075            LogicExpr::NeoEvent(data) => Some(data.verb),
2076            // Control structures directly contain the verb
2077            LogicExpr::Control { verb, .. } => Some(*verb),
2078            // Phase 46: For BinaryOp, try to find NeoEvent first (either side),
2079            // then fall back to Predicate. This handles both:
2080            // - Transitive: Apple(x) ∧ ∃e(Eat(e)...) - NeoEvent on right
2081            // - Motion PP: ∃e(Walk(e)...) ∧ To(e, Park) - NeoEvent on left
2082            LogicExpr::BinaryOp { left, right, .. } => {
2083                // First check if left contains a NeoEvent (motion PP case)
2084                if let Some(verb) = self.extract_neo_event_verb(left) {
2085                    return Some(verb);
2086                }
2087                // Then check right (transitive case with type predicate on left)
2088                if let Some(verb) = self.extract_neo_event_verb(right) {
2089                    return Some(verb);
2090                }
2091                // Fall back to any extractable verb
2092                self.extract_verb_from_expr(left)
2093                    .or_else(|| self.extract_verb_from_expr(right))
2094            }
2095            // Plain predicate - last resort (might be type predicate or PP)
2096            LogicExpr::Predicate { name, .. } => Some(*name),
2097            LogicExpr::Modal { operand, .. } => self.extract_verb_from_expr(operand),
2098            LogicExpr::Presupposition { assertion, .. } => self.extract_verb_from_expr(assertion),
2099            LogicExpr::Temporal { body, .. } => self.extract_verb_from_expr(body),
2100            LogicExpr::TemporalAnchor { body, .. } => self.extract_verb_from_expr(body),
2101            LogicExpr::Aspectual { body, .. } => self.extract_verb_from_expr(body),
2102            LogicExpr::Quantifier { body, .. } => self.extract_verb_from_expr(body),
2103            _ => None,
2104        }
2105    }
2106
2107    /// Phase 46: Generalized gapping with template-guided reconstruction.
2108    /// Handles NPs, PPs, temporal adverbs, and preserves roles from EventTemplate.
2109    fn parse_gapped_clause(&mut self, borrowed_verb: Symbol) -> ParseResult<&'a LogicExpr<'a>> {
2110        let subject = self.parse_noun_phrase(true)?;
2111
2112        if self.check(&TokenType::Comma) {
2113            self.advance();
2114        }
2115
2116        let subject_term = self.noun_phrase_to_term(&subject);
2117        let event_var = self.get_event_var();
2118        let suppress_existential = self.drs.in_conditional_antecedent();
2119
2120        // Get template for role guidance
2121        let template = self.last_event_template.clone();
2122
2123        // Collect arguments (NPs, PPs, temporal adverbs) from gapped clause
2124        let mut np_args: Vec<Term<'a>> = Vec::new();
2125        let mut pp_args: Vec<(Symbol, Term<'a>)> = Vec::new();
2126        let mut override_adverb: Option<Symbol> = None;
2127
2128        loop {
2129            if self.check_temporal_adverb() {
2130                // Temporal adverb: override template modifier
2131                if let TokenType::TemporalAdverb(sym) = self.advance().kind {
2132                    override_adverb = Some(sym);
2133                }
2134            } else if self.check_preposition() {
2135                // PP argument: "to the school", "on the table"
2136                let prep = if let TokenType::Preposition(sym) = self.advance().kind {
2137                    sym
2138                } else {
2139                    continue;
2140                };
2141                let np = self.parse_noun_phrase(false)?;
2142                pp_args.push((prep, self.noun_phrase_to_term(&np)));
2143            } else if self.check_content_word() || self.check_article() {
2144                // NP argument
2145                let np = self.parse_noun_phrase(false)?;
2146                np_args.push(self.noun_phrase_to_term(&np));
2147                if self.check(&TokenType::Comma) {
2148                    self.advance();
2149                }
2150            } else {
2151                break;
2152            }
2153        }
2154
2155        // Build roles using template guidance
2156        let roles = self.build_gapped_roles(subject_term, &np_args, &pp_args, &template);
2157
2158        // Handle modifiers: override if adverb provided, else inherit from template
2159        let modifiers = match (override_adverb, &template) {
2160            (Some(adv), Some(tmpl)) => {
2161                // Filter out temporal modifiers from template, add new one
2162                let mut mods: Vec<Symbol> = tmpl
2163                    .modifiers
2164                    .iter()
2165                    .filter(|m| !self.is_temporal_modifier(**m))
2166                    .cloned()
2167                    .collect();
2168                mods.push(adv);
2169                mods
2170            }
2171            (Some(adv), None) => vec![adv],
2172            (None, Some(tmpl)) => tmpl.modifiers.clone(),
2173            (None, None) => vec![],
2174        };
2175
2176        Ok(self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
2177            event_var,
2178            verb: borrowed_verb,
2179            roles: self.ctx.roles.alloc_slice(roles),
2180            modifiers: self.ctx.syms.alloc_slice(modifiers),
2181            suppress_existential,
2182            world: None,
2183        }))))
2184    }
2185
2186    fn is_complete_clause(&self, expr: &LogicExpr<'a>) -> bool {
2187        match expr {
2188            LogicExpr::Atom(_) => false,
2189            LogicExpr::Predicate { .. } => true,
2190            LogicExpr::Quantifier { .. } => true,
2191            LogicExpr::Modal { .. } => true,
2192            LogicExpr::Temporal { .. } => true,
2193            LogicExpr::Aspectual { .. } => true,
2194            LogicExpr::BinaryOp { .. } => true,
2195            LogicExpr::UnaryOp { .. } => true,
2196            LogicExpr::Control { .. } => true,
2197            LogicExpr::Presupposition { .. } => true,
2198            LogicExpr::Categorical(_) => true,
2199            LogicExpr::Relation(_) => true,
2200            _ => true,
2201        }
2202    }
2203
2204    /// Parse disjunction (Or/Iff) - lowest precedence logical connectives.
2205    /// Calls parse_conjunction for operands to ensure And binds tighter.
2206    fn parse_disjunction(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
2207        let mut expr = self.parse_conjunction()?;
2208
2209        while self.check(&TokenType::Comma)
2210            || self.check(&TokenType::Or)
2211        {
2212            if self.check(&TokenType::Comma) {
2213                self.advance();
2214            }
2215            // Iff is handled at a LOOSER precedence tier below (standard
2216            // precedence ∨ > ↔); only Or folds here.
2217            if !self.match_token(&[TokenType::Or]) {
2218                break;
2219            }
2220            let operator = self.previous().kind.clone();
2221            self.current_island += 1;
2222
2223            let saved_pos = self.current;
2224            let standard_attempt = self.try_parse(|p| p.parse_conjunction());
2225
2226            // Gapping in disjunction: only for Or, not Iff. Use original (non-expanded) trigger.
2227            // Expanded gapping (with Period/is_at_end) only applies in parse_conjunction.
2228            let use_gapping = match &standard_attempt {
2229                Some(right) => {
2230                    !self.is_complete_clause(right)
2231                        && (self.check(&TokenType::Comma) || self.check_content_word())
2232                        && operator != TokenType::Iff // Don't gap on biconditional
2233                }
2234                None => operator != TokenType::Iff, // For Iff, require successful parse
2235            };
2236
2237            if !use_gapping {
2238                if let Some(right) = standard_attempt {
2239                    expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2240                        left: expr,
2241                        op: operator,
2242                        right,
2243                    });
2244                }
2245            } else {
2246                self.current = saved_pos;
2247
2248                let borrowed_verb = self.extract_verb_from_expr(expr).ok_or(ParseError {
2249                    kind: ParseErrorKind::GappingResolutionFailed,
2250                    span: self.current_span(),
2251                })?;
2252
2253                let right = self.parse_gapped_clause(borrowed_verb)?;
2254
2255                expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2256                    left: expr,
2257                    op: operator,
2258                    right,
2259                });
2260            }
2261        }
2262
2263        // Handle binary temporal connectives (lowest precedence temporal)
2264        // "P until Q" → TemporalBinary { Until, P, Q }
2265        // "P release Q" → TemporalBinary { Release, P, Q }
2266        // "P weak-until Q" → TemporalBinary { WeakUntil, P, Q }
2267        if self.check(&TokenType::Until) || self.check(&TokenType::Release) || self.check(&TokenType::WeakUntil) {
2268            let op = match self.peek().kind {
2269                TokenType::Release => crate::ast::logic::BinaryTemporalOp::Release,
2270                TokenType::WeakUntil => crate::ast::logic::BinaryTemporalOp::WeakUntil,
2271                _ => crate::ast::logic::BinaryTemporalOp::Until,
2272            };
2273            self.advance();
2274            let right = self.parse_conjunction()?;
2275            expr = self.ctx.exprs.alloc(LogicExpr::TemporalBinary {
2276                operator: op,
2277                left: expr,
2278                right,
2279            });
2280        }
2281
2282        // Check for trailing "within N cycles" bounded temporal delay
2283        let expr = self.try_wrap_bounded_delay(expr);
2284
2285        // Trailing "in the next cycle" → X(P)
2286        let mut expr = self.try_wrap_next_cycle(expr);
2287
2288        // Sentence-final temporal anchor ("…eat Bill first.", "…left
2289        // yesterday.") for clauses whose path didn't consume it in the VP.
2290        if let TokenType::TemporalAdverb(anchor) = self.peek().kind {
2291            if matches!(
2292                self.tokens.get(self.current + 1).map(|t| &t.kind),
2293                Some(TokenType::Period) | Some(TokenType::Exclamation) | Some(TokenType::EOF) | None
2294            ) {
2295                self.advance();
2296                expr = self.ctx.exprs.alloc(LogicExpr::TemporalAnchor { anchor, body: expr });
2297            }
2298        }
2299
2300        // Postposed necessary condition: "Y only when X." / "Y only if X." ⇔ Y → X (X is
2301        // *necessary* for Y — the textbook reading of "only if"). This is the converse direction
2302        // of the sufficient "Y when X" below, so it must be matched first.
2303        if self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("only")
2304            && matches!(
2305                self.tokens.get(self.current + 1).map(|t| &t.kind),
2306                Some(TokenType::When) | Some(TokenType::If)
2307            )
2308        {
2309            self.advance(); // only
2310            self.advance(); // when | if
2311            let condition = self.parse_conjunction()?;
2312            expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2313                left: expr,
2314                op: TokenType::If,
2315                right: condition,
2316            });
2317        }
2318        // Postposed "when": "Y when X." ⇔ "When X, Y." → X → Y
2319        else if self.check(&TokenType::When) {
2320            self.advance();
2321            let condition = self.parse_conjunction()?;
2322            expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2323                left: condition,
2324                op: TokenType::Implies,
2325                right: expr,
2326            });
2327        }
2328
2329        // Biconditional binds LOOSER than disjunction (standard precedence
2330        // ∨ > ↔). Fold any trailing `iff` with a FULL disjunction as its right
2331        // operand, so "P if and only if Q or R" is P ↔ (Q ∨ R), not (P ↔ Q) ∨ R.
2332        while self.check(&TokenType::Iff) {
2333            self.advance();
2334            self.current_island += 1;
2335            let right = self.parse_disjunction()?;
2336            expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2337                left: expr,
2338                op: TokenType::Iff,
2339                right,
2340            });
2341        }
2342
2343        Ok(expr)
2344    }
2345
2346    /// Parse conjunction (And) - higher precedence than Or.
2347    /// Calls parse_atom for operands.
2348    /// Extracts the subject of a copular predication (the first `Constant` argument
2349    /// of a copular `Predicate`), digging through degree/aspect/boolean wrappers.
2350    /// Returns `None` for event predications (NeoEvent) and variable subjects, so
2351    /// only true copular clauses ("X is ADJ/NP") trigger predicate coordination.
2352    fn extract_copular_subject(&self, expr: &'a LogicExpr<'a>) -> Option<Symbol> {
2353        match expr {
2354            LogicExpr::Predicate { args, .. } => match args.first() {
2355                Some(Term::Constant(s)) => Some(*s),
2356                _ => None,
2357            },
2358            LogicExpr::Quantifier { body, .. } => self.extract_copular_subject(body),
2359            LogicExpr::Aspectual { body, .. } => self.extract_copular_subject(body),
2360            LogicExpr::UnaryOp { operand, .. } => self.extract_copular_subject(operand),
2361            LogicExpr::BinaryOp { left, .. } => self.extract_copular_subject(left),
2362            _ => None,
2363        }
2364    }
2365
2366    /// Parses a bare copular-predicate remnant — an adjective ("wealthy") or a
2367    /// predicate nominal ("a philanthropist") — as `Predicate(subject)`. Returns
2368    /// `None` (consuming nothing) if the next tokens are not such a remnant.
2369    fn try_parse_copular_predicate(
2370        &mut self,
2371        subject: Symbol,
2372    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
2373        let pred_sym = if let TokenType::Adjective(adj) = self.peek().kind {
2374            // An adjective-classified word followed by a copula is the
2375            // SUBJECT of a new clause ("…and ready is not asserted"), not a
2376            // predicate remnant of the previous one.
2377            if matches!(
2378                self.tokens.get(self.current + 1).map(|t| &t.kind),
2379                Some(TokenType::Is)
2380                    | Some(TokenType::Are)
2381                    | Some(TokenType::Was)
2382                    | Some(TokenType::Were)
2383            ) {
2384                return Ok(None);
2385            }
2386            self.advance();
2387            adj
2388        } else if self.check_article() {
2389            // "a/an N" predicate nominal — parse the NP and use its head noun.
2390            let np = self.parse_noun_phrase(false)?;
2391            np.noun
2392        } else {
2393            return Ok(None);
2394        };
2395        Ok(Some(self.ctx.exprs.alloc(LogicExpr::Predicate {
2396            name: pred_sym,
2397            args: self.ctx.terms.alloc_slice([Term::Constant(subject)]),
2398            world: None,
2399        })))
2400    }
2401
2402    fn parse_conjunction(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
2403        let mut expr = self.parse_atom()?;
2404
2405        // Handle causal "because" at conjunction level
2406        // Phase 35: Do NOT consume if followed by string literal (Trust justification)
2407        if self.check(&TokenType::Because) && !self.peek_next_is_string_literal() {
2408            self.advance();
2409            let cause = self.parse_atom()?;
2410            return Ok(self.ctx.exprs.alloc(LogicExpr::Causal {
2411                effect: expr,
2412                cause,
2413            }));
2414        }
2415
2416        while self.check(&TokenType::Comma) || self.check(&TokenType::And) {
2417            if self.check(&TokenType::Comma) {
2418                self.advance();
2419            }
2420            if !self.match_token(&[TokenType::And]) {
2421                break;
2422            }
2423            let operator = self.previous().kind.clone();
2424            self.current_island += 1;
2425
2426            // Non-parallel copular coordination (§2.2): "X is wealthy and a
2427            // philanthropist" — the remnant after "and" is a bare predicate (an
2428            // adjective or a predicate nominal) attributed to the SAME copular
2429            // subject, not a gapped event verb.
2430            if let Some(subj) = self.extract_copular_subject(expr) {
2431                let cop_pos = self.current;
2432                if let Some(p2) = self.try_parse_copular_predicate(subj)? {
2433                    expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2434                        left: expr,
2435                        op: operator,
2436                        right: p2,
2437                    });
2438                    continue;
2439                }
2440                self.current = cop_pos;
2441            }
2442
2443            let saved_pos = self.current;
2444            let standard_attempt = self.try_parse(|p| p.parse_atom());
2445
2446            // Phase 46: Expanded gapping trigger to support PP gapping, temporal override,
2447            // and intransitive gapping (bare subject at clause boundary)
2448            let use_gapping = match &standard_attempt {
2449                Some(right) => {
2450                    !self.is_complete_clause(right)
2451                        && (self.check(&TokenType::Comma)
2452                            || self.check_content_word()
2453                            || self.check_preposition()
2454                            || self.check_temporal_adverb()
2455                            || self.check(&TokenType::Period)
2456                            || self.is_at_end())
2457                }
2458                None => true,
2459            };
2460
2461            if !use_gapping {
2462                if let Some(right) = standard_attempt {
2463                    expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2464                        left: expr,
2465                        op: operator,
2466                        right,
2467                    });
2468                }
2469            } else {
2470                self.current = saved_pos;
2471
2472                let borrowed_verb = self.extract_verb_from_expr(expr).ok_or(ParseError {
2473                    kind: ParseErrorKind::GappingResolutionFailed,
2474                    span: self.current_span(),
2475                })?;
2476
2477                let right = self.parse_gapped_clause(borrowed_verb)?;
2478
2479                expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2480                    left: expr,
2481                    op: operator,
2482                    right,
2483                });
2484            }
2485        }
2486
2487        Ok(expr)
2488    }
2489
2490    fn parse_relative_clause(&mut self, gap_var: Symbol) -> ParseResult<&'a LogicExpr<'a>> {
2491        // A clause-initial adverb ("who FIRST started in 1983", "who ORIGINALLY came
2492        // out in 1866") modifies the relative clause's event. It hid the verb from the
2493        // dispatch below, stranding the clue (TrailingTokens) or dropping the clause to
2494        // `?`. Consume it, parse the rest of the clause, then conjoin the adverb over
2495        // the gap so nothing is lost. Only when a clause predicate (verb / perfect /
2496        // modal / auxiliary / copula / negation) actually follows the adverb.
2497        if matches!(
2498            self.peek().kind,
2499            TokenType::Adverb(_) | TokenType::TemporalAdverb(_)
2500        ) {
2501            let next_opens_predicate = self.tokens.get(self.current + 1).map_or(false, |t| {
2502                self.kind_is_verb(&t.kind)
2503                    || matches!(
2504                        t.kind,
2505                        TokenType::Had
2506                            | TokenType::Auxiliary(_)
2507                            | TokenType::Not
2508                            | TokenType::Is
2509                            | TokenType::Are
2510                            | TokenType::Was
2511                            | TokenType::Were
2512                            | TokenType::Can
2513                            | TokenType::Could
2514                            | TokenType::Must
2515                            | TokenType::Should
2516                            | TokenType::May
2517                            | TokenType::Might
2518                            | TokenType::Would
2519                            | TokenType::Shall
2520                            | TokenType::Cannot
2521                    )
2522            });
2523            if next_opens_predicate {
2524                let adv = match self.peek().kind {
2525                    TokenType::Adverb(s) | TokenType::TemporalAdverb(s) => s,
2526                    _ => unreachable!(),
2527                };
2528                self.advance();
2529                let rest = self.parse_relative_clause(gap_var)?;
2530                let adv_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2531                    name: adv,
2532                    args: self.ctx.terms.alloc_slice([Term::Variable(gap_var)]),
2533                    world: None,
2534                });
2535                return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2536                    left: rest,
2537                    op: TokenType::And,
2538                    right: adv_pred,
2539                }));
2540            }
2541        }
2542
2543        // "who had the port" — possessive HAVE (past) as the clause verb, NOT a
2544        // perfect auxiliary (no participle follows); re-tag so check_verb handles it.
2545        if self.check(&TokenType::Had)
2546            && !matches!(
2547                self.tokens.get(self.current + 1).map(|t| &t.kind),
2548                Some(TokenType::Verb { .. })
2549            )
2550        {
2551            let have_lemma = self.interner.intern("Have");
2552            self.tokens[self.current].kind = TokenType::Verb {
2553                lemma: have_lemma,
2554                time: Time::Past,
2555                aspect: crate::lexicon::Aspect::Simple,
2556                class: crate::lexicon::VerbClass::State,
2557            };
2558        }
2559
2560        // "who did 49 jumps" / "who does the dishes" — main verb "do" (performed),
2561        // NOT do-support (no verb follows); re-tag so check_verb handles it.
2562        if matches!(self.peek().kind, TokenType::Auxiliary(_))
2563            && !matches!(
2564                self.tokens.get(self.current + 1).map(|t| &t.kind),
2565                Some(TokenType::Verb { .. })
2566            )
2567        {
2568            let lex = self.interner.resolve(self.peek().lexeme).to_lowercase();
2569            if matches!(lex.as_str(), "did" | "do" | "does") {
2570                let do_lemma = self.interner.intern("Do");
2571                let time = if lex == "did" { Time::Past } else { Time::Present };
2572                self.tokens[self.current].kind = TokenType::Verb {
2573                    lemma: do_lemma,
2574                    time,
2575                    aspect: crate::lexicon::Aspect::Simple,
2576                    class: crate::lexicon::VerbClass::Activity,
2577                };
2578            }
2579        }
2580
2581        // Perfect-aspect relative clause: "who HAS done 49 jumps", "who HAVE won",
2582        // "that HAD been issued in 1868". The perfect auxiliary + a participle is an
2583        // aspect chain over the gap, NOT possessive HAVE — which the `check_verb` below
2584        // would greedily consume, stranding the participle (TrailingTokens). Mirror the
2585        // main-clause perfect dispatch (`parse_aspect_chain`). The participle may be an
2586        // Ambiguous noun/verb ("done"), so judge it with `kind_is_verb`, and allow an
2587        // intervening negation ("who has not won").
2588        let head_word = self.interner.resolve(self.peek().lexeme).to_lowercase();
2589        let is_perfect_head =
2590            matches!(head_word.as_str(), "has" | "have") || self.check(&TokenType::Had);
2591        let next_opens_participle = self.tokens.get(self.current + 1).map_or(false, |t| {
2592            self.kind_is_verb(&t.kind) || matches!(t.kind, TokenType::Not)
2593        });
2594        if is_perfect_head && next_opens_participle {
2595            return self.parse_aspect_chain_with_term(Term::Variable(gap_var));
2596        }
2597
2598        if self.check_verb() {
2599            return self.parse_verb_phrase_for_restriction(gap_var);
2600        }
2601
2602        // Modal-headed relative clause: "that can fly for 40 minutes", "who must
2603        // attend", "that should win". The modal scopes the event over the gap
2604        // variable — "the device that can fly" → ◇ ∃e(Fly(e) ∧ Agent(e, x)).
2605        if self.check_modal() {
2606            return self.parse_aspect_chain_with_term(Term::Variable(gap_var));
2607        }
2608
2609        // Auxiliary-headed relative clause: "who will be studying radiation",
2610        // "who would win". Record the modality/tense, drop an optional "be" of
2611        // the progressive, then parse the verb phrase as the restriction.
2612        if let TokenType::Auxiliary(time) = self.peek().kind {
2613            self.advance(); // "will" / "would" / "did"
2614            // Drop the progressive's "be" ("will be studying"). It tokenizes
2615            // either as TokenType::Be or as a verb with lemma "be".
2616            let is_be = self.check(&TokenType::Be)
2617                || matches!(self.peek().kind, TokenType::Verb { lemma, .. }
2618                    if self.interner.resolve(lemma).eq_ignore_ascii_case("be"));
2619            if is_be {
2620                self.advance();
2621            }
2622            if self.check_verb() {
2623                let restriction = self.parse_verb_phrase_for_restriction(gap_var)?;
2624                return Ok(if time == Time::Future {
2625                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2626                        operator: TemporalOperator::Future,
2627                        body: restriction,
2628                    })
2629                } else {
2630                    restriction
2631                });
2632            }
2633            // "who will be ready" — copular future with an adjective/noun.
2634            if self.check_content_word() || self.check_article() {
2635                let pred_np = self.parse_noun_phrase(false)?;
2636                let base = self.ctx.exprs.alloc(LogicExpr::Predicate {
2637                    name: pred_np.noun,
2638                    args: self.ctx.terms.alloc_slice([Term::Variable(gap_var)]),
2639                    world: None,
2640                });
2641                return Ok(if time == Time::Future {
2642                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2643                        operator: TemporalOperator::Future,
2644                        body: base,
2645                    })
2646                } else {
2647                    base
2648                });
2649            }
2650        }
2651
2652        // Copular relative: "that is on the table", "that is red".
2653        if matches!(
2654            self.peek().kind,
2655            TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
2656        ) {
2657            let copula_past = matches!(self.peek().kind, TokenType::Was | TokenType::Were);
2658            self.advance(); // copula
2659            let negated = self.check(&TokenType::Not);
2660            if negated {
2661                self.advance();
2662            }
2663            // A temporal adverb after the copula ("who is NOW with the Tigers", "that
2664            // was THEN the leader") frames the predication; consume it and conjoin it
2665            // over the gap once the complement is built, so it is not stranded
2666            // (ExpectedContentWord at the adverb).
2667            let rel_temporal_adv = if let TokenType::TemporalAdverb(s) = self.peek().kind {
2668                self.advance();
2669                Some(s)
2670            } else {
2671                None
2672            };
2673            // "that is printing 100 pages", "that is paying the rent" — a
2674            // PROGRESSIVE verb after the copula is a verb phrase (∃e(Print(e) ∧
2675            // Agent(e,x) ∧ Theme(e,…))), not a predicate adjective. Gate strictly
2676            // on Progressive aspect so a PASSIVE past participle ("that was ISSUED
2677            // in 1868") is NOT mis-read as an active VP — that stays the
2678            // passive/PP path below.
2679            if matches!(
2680                self.peek().kind,
2681                TokenType::Verb { aspect: crate::lexicon::Aspect::Progressive, .. }
2682            ) {
2683                let vp = self.parse_verb_phrase_for_restriction(gap_var)?;
2684                let vp = self.conjoin_relative_temporal_adverb(vp, gap_var, rel_temporal_adv);
2685                let vp = if negated {
2686                    self.ctx.exprs.alloc(LogicExpr::UnaryOp { op: TokenType::Not, operand: vp })
2687                } else {
2688                    vp
2689                };
2690                return Ok(if copula_past {
2691                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2692                        operator: TemporalOperator::Past,
2693                        body: vp,
2694                    })
2695                } else {
2696                    vp
2697                });
2698            }
2699            let pred: &'a LogicExpr<'a> = if self.check_preposition() {
2700                let prep = if let TokenType::Preposition(sym) = self.advance().kind {
2701                    sym
2702                } else {
2703                    self.interner.intern("At")
2704                };
2705                let obj = self.parse_noun_phrase(false)?;
2706                self.ctx.exprs.alloc(LogicExpr::Predicate {
2707                    name: prep,
2708                    args: self
2709                        .ctx
2710                        .terms
2711                        .alloc_slice([Term::Variable(gap_var), Term::Constant(obj.noun)]),
2712                    world: None,
2713                })
2714            } else if self.check_number() {
2715                // Measure complement: "that is 30 inches long" → Long(x, 30 inches);
2716                // bare "that is 30 inches" → Measure(x, 30 inches).
2717                let measure = self.parse_measure_phrase()?;
2718                let dim = if self.check_content_word() {
2719                    self.consume_content_word()?
2720                } else {
2721                    self.interner.intern("Measure")
2722                };
2723                self.ctx.exprs.alloc(LogicExpr::Predicate {
2724                    name: dim,
2725                    args: self
2726                        .ctx
2727                        .terms
2728                        .alloc_slice([Term::Variable(gap_var), *measure]),
2729                    world: None,
2730                })
2731            } else {
2732                let adj = self.consume_content_word()?;
2733                self.ctx.exprs.alloc(LogicExpr::Predicate {
2734                    name: adj,
2735                    args: self.ctx.terms.alloc_slice([Term::Variable(gap_var)]),
2736                    world: None,
2737                })
2738            };
2739            // Trailing PPs on a copular relative ("that was issued in 1868",
2740            // "that is from Spain") — conjoin each as a predicate over the gap.
2741            let mut pred = pred;
2742            while self.check_preposition() {
2743                let prep = if let TokenType::Preposition(s) = self.advance().kind {
2744                    s
2745                } else {
2746                    break;
2747                };
2748                let obj_term = if self.check_number() {
2749                    *self.parse_measure_phrase()?
2750                } else if self.check_content_word() || self.check_article() {
2751                    Term::Constant(self.parse_noun_phrase(false)?.noun)
2752                } else {
2753                    self.current -= 1;
2754                    break;
2755                };
2756                let pp = self.ctx.exprs.alloc(LogicExpr::Predicate {
2757                    name: prep,
2758                    args: self.ctx.terms.alloc_slice([Term::Variable(gap_var), obj_term]),
2759                    world: None,
2760                });
2761                pred = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2762                    left: pred,
2763                    op: TokenType::And,
2764                    right: pp,
2765                });
2766            }
2767            let pred = self.conjoin_relative_temporal_adverb(pred, gap_var, rel_temporal_adv);
2768            let pred = if copula_past {
2769                &*self.ctx.exprs.alloc(LogicExpr::Temporal {
2770                    operator: TemporalOperator::Past,
2771                    body: pred,
2772                })
2773            } else {
2774                pred
2775            };
2776            return Ok(if negated {
2777                self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2778                    op: TokenType::Not,
2779                    operand: pred,
2780                })
2781            } else {
2782                pred
2783            });
2784        }
2785
2786        // Handle "do/does (not)" in relative clauses: "who do not shave themselves"
2787        if self.check(&TokenType::Do) || self.check(&TokenType::Does) {
2788            self.advance(); // consume "do/does"
2789
2790            let is_negated = self.check(&TokenType::Not);
2791            if is_negated {
2792                self.advance(); // consume "not"
2793            }
2794
2795            if self.check_verb() {
2796                let verb = self.consume_verb();
2797
2798                // Check for reflexive object: "shave themselves"
2799                let roles = if self.check(&TokenType::Reflexive) {
2800                    self.advance(); // consume "themselves/himself"
2801                    vec![
2802                        (ThematicRole::Agent, Term::Variable(gap_var)),
2803                        (ThematicRole::Theme, Term::Variable(gap_var)),
2804                    ]
2805                } else if self.check_content_word() || self.check_article() {
2806                    // Parse object NP
2807                    let obj = self.parse_noun_phrase(false)?;
2808                    vec![
2809                        (ThematicRole::Agent, Term::Variable(gap_var)),
2810                        (ThematicRole::Theme, Term::Constant(obj.noun)),
2811                    ]
2812                } else {
2813                    // Intransitive
2814                    vec![(ThematicRole::Agent, Term::Variable(gap_var))]
2815                };
2816
2817                let event_var = self.get_event_var();
2818                let suppress_existential = self.drs.in_conditional_antecedent();
2819                let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
2820                    event_var,
2821                    verb,
2822                    roles: self.ctx.roles.alloc_slice(roles),
2823                    modifiers: self.ctx.syms.alloc_slice(vec![]),
2824                    suppress_existential,
2825                    world: None,
2826                })));
2827
2828                if is_negated {
2829                    return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2830                        op: TokenType::Not,
2831                        operand: event,
2832                    }));
2833                }
2834                return Ok(event);
2835            }
2836        }
2837
2838        if self.check_content_word() || self.check_article() {
2839            let rel_subject = self.parse_noun_phrase_for_relative()?;
2840
2841            let nested_relative = if matches!(self.peek().kind, TokenType::Article(_)) {
2842                let nested_var = self.next_var_name();
2843                Some((nested_var, self.parse_relative_clause(nested_var)?))
2844            } else {
2845                None
2846            };
2847
2848            if self.check_verb() {
2849                let verb = self.consume_verb();
2850
2851                // A STRANDED preposition ("the animal Eva works WITH", "the case Bob
2852                // paid FOR") makes the GAP the object of that preposition, not the
2853                // verb's direct Theme. Detect a preposition with NO overt object (the
2854                // matrix clause follows immediately) and bind the gap to it below.
2855                let stranded_prep: Option<Symbol> = if self.check_preposition()
2856                    && !self.check_to_preposition()
2857                {
2858                    let has_overt_object = matches!(
2859                        self.tokens.get(self.current + 1).map(|t| &t.kind),
2860                        Some(TokenType::Article(_))
2861                            | Some(TokenType::Noun(_))
2862                            | Some(TokenType::ProperName(_))
2863                            | Some(TokenType::Number(_))
2864                            | Some(TokenType::Cardinal(_))
2865                            | Some(TokenType::Pronoun { .. })
2866                            | Some(TokenType::Possessive)
2867                    );
2868                    let prep_sym = match &self.peek().kind {
2869                        TokenType::Preposition(s) => Some(*s),
2870                        _ => None,
2871                    };
2872                    if !has_overt_object && prep_sym.is_some() {
2873                        self.advance();
2874                        prep_sym
2875                    } else {
2876                        None
2877                    }
2878                } else {
2879                    None
2880                };
2881
2882                let mut roles: Vec<(ThematicRole, Term<'a>)> =
2883                    vec![(ThematicRole::Agent, Term::Constant(rel_subject.noun))];
2884                if stranded_prep.is_none() {
2885                    roles.push((ThematicRole::Theme, Term::Variable(gap_var)));
2886                }
2887
2888                while self.check_to_preposition() {
2889                    self.advance();
2890                    if self.check_content_word() || self.check_article() {
2891                        let recipient = self.parse_noun_phrase(false)?;
2892                        roles.push((ThematicRole::Recipient, Term::Constant(recipient.noun)));
2893                    }
2894                }
2895
2896                let event_var = self.get_event_var();
2897
2898                // Absorb the embedded clause's trailing PP complements onto the
2899                // EVENT ("photographed in 1989" → In(e, 1989), "issued at the depot"
2900                // → At(e, depot)) so an object-gap reduced relative does not strand
2901                // them. They attach over the event var, inside its existential scope.
2902                let mut pp_preds: Vec<&'a LogicExpr<'a>> = Vec::new();
2903                // The stranded preposition's object IS the gap ("Eva works with [x]" →
2904                // With(e, x)); attach it over the event so it falls in the event scope.
2905                if let Some(prep) = stranded_prep {
2906                    pp_preds.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
2907                        name: prep,
2908                        args: self
2909                            .ctx
2910                            .terms
2911                            .alloc_slice([Term::Variable(event_var), Term::Variable(gap_var)]),
2912                        world: None,
2913                    }));
2914                }
2915                while self.check_preposition() {
2916                    let prep = if let TokenType::Preposition(s) = self.advance().kind {
2917                        s
2918                    } else {
2919                        break;
2920                    };
2921                    let obj_term = if self.check_number() {
2922                        *self.parse_measure_phrase()?
2923                    } else if self.check_content_word() || self.check_article() {
2924                        Term::Constant(self.parse_noun_phrase(false)?.noun)
2925                    } else {
2926                        self.current -= 1;
2927                        break;
2928                    };
2929                    pp_preds.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
2930                        name: prep,
2931                        args: self.ctx.terms.alloc_slice([Term::Variable(event_var), obj_term]),
2932                        world: None,
2933                    }));
2934                }
2935                let has_pps = !pp_preds.is_empty();
2936
2937                // With PPs, suppress the NeoEvent's own ∃e and wrap an explicit one so
2938                // the PP conjuncts fall inside the event's scope; without PPs, the
2939                // NeoEvent emits its own ∃e exactly as before (byte-identical).
2940                let suppress_existential = self.drs.in_conditional_antecedent() || has_pps;
2941                let neo = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
2942                    event_var,
2943                    verb,
2944                    roles: self.ctx.roles.alloc_slice(roles),
2945                    modifiers: self.ctx.syms.alloc_slice(vec![]),
2946                    suppress_existential,
2947                    world: None,
2948                })));
2949                let mut event_body = neo;
2950                for pp in pp_preds {
2951                    event_body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2952                        left: event_body,
2953                        op: TokenType::And,
2954                        right: pp,
2955                    });
2956                }
2957                let this_event: &'a LogicExpr<'a> = if has_pps {
2958                    self.ctx.exprs.alloc(LogicExpr::Quantifier {
2959                        kind: crate::ast::QuantifierKind::Existential,
2960                        variable: event_var,
2961                        body: event_body,
2962                        island_id: self.current_island,
2963                    })
2964                } else {
2965                    event_body
2966                };
2967
2968                if let Some((nested_var, nested_clause)) = nested_relative {
2969                    let type_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2970                        name: rel_subject.noun,
2971                        args: self.ctx.terms.alloc_slice([Term::Variable(nested_var)]),
2972                        world: None,
2973                    });
2974
2975                    let inner = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2976                        left: type_pred,
2977                        op: TokenType::And,
2978                        right: nested_clause,
2979                    });
2980
2981                    let combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2982                        left: inner,
2983                        op: TokenType::And,
2984                        right: this_event,
2985                    });
2986
2987                    return Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
2988                        kind: crate::ast::QuantifierKind::Existential,
2989                        variable: nested_var,
2990                        body: combined,
2991                        island_id: self.current_island,
2992                    }));
2993                }
2994
2995                return Ok(this_event);
2996            }
2997        }
2998
2999        if self.check_verb() {
3000            return self.parse_verb_phrase_for_restriction(gap_var);
3001        }
3002
3003        let unknown = self.interner.intern("?");
3004        Ok(self.ctx.exprs.alloc(LogicExpr::Atom(unknown)))
3005    }
3006
3007    fn check_ellipsis_auxiliary(&self) -> bool {
3008        matches!(
3009            self.peek().kind,
3010            TokenType::Does | TokenType::Do |
3011            TokenType::Can | TokenType::Could | TokenType::Would |
3012            TokenType::May | TokenType::Must | TokenType::Should
3013        )
3014    }
3015
3016    fn check_ellipsis_terminator(&self) -> bool {
3017        if self.is_at_end() || self.check(&TokenType::Period) {
3018            return true;
3019        }
3020        if self.check_content_word() {
3021            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
3022            return word == "too" || word == "also";
3023        }
3024        false
3025    }
3026
3027    fn try_parse_ellipsis(&mut self) -> Option<ParseResult<&'a LogicExpr<'a>>> {
3028        // Need a stored template to reconstruct from
3029        if self.last_event_template.is_none() {
3030            return None;
3031        }
3032
3033        let saved_pos = self.current;
3034
3035        // Pattern: Subject + Auxiliary + (not)? + Terminator
3036        // Subject must be proper name or pronoun
3037        let subject_sym = if matches!(self.peek().kind, TokenType::ProperName(_)) {
3038            if let TokenType::ProperName(sym) = self.advance().kind {
3039                sym
3040            } else {
3041                self.current = saved_pos;
3042                return None;
3043            }
3044        } else if self.check_pronoun() {
3045            let token = self.advance().clone();
3046            if let TokenType::Pronoun { gender, number, .. } = token.kind {
3047                match self.resolve_pronoun(gender, number) {
3048                    Ok(resolved) => match resolved {
3049                        super::ResolvedPronoun::Variable(s) | super::ResolvedPronoun::Constant(s) => s,
3050                    },
3051                    Err(e) => return Some(Err(e)),
3052                }
3053            } else {
3054                self.current = saved_pos;
3055                return None;
3056            }
3057        } else {
3058            return None;
3059        };
3060
3061        // Must be followed by ellipsis auxiliary
3062        if !self.check_ellipsis_auxiliary() {
3063            self.current = saved_pos;
3064            return None;
3065        }
3066        let aux_token = self.advance().kind.clone();
3067
3068        // Check for negation
3069        let is_negated = self.match_token(&[TokenType::Not]);
3070
3071        // Must end with terminator
3072        if !self.check_ellipsis_terminator() {
3073            self.current = saved_pos;
3074            return None;
3075        }
3076
3077        // Consume "too"/"also" if present
3078        if self.check_content_word() {
3079            let word = self.interner.resolve(self.peek().lexeme).to_lowercase();
3080            if word == "too" || word == "also" {
3081                self.advance();
3082            }
3083        }
3084
3085        // Reconstruct from template
3086        let template = self.last_event_template.clone().unwrap();
3087        let event_var = self.get_event_var();
3088        let suppress_existential = self.drs.in_conditional_antecedent();
3089
3090        // Build roles with new subject as Agent
3091        let mut roles: Vec<(ThematicRole, Term<'a>)> = vec![
3092            (ThematicRole::Agent, Term::Constant(subject_sym))
3093        ];
3094        roles.extend(template.non_agent_roles.iter().cloned());
3095
3096        let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3097            event_var,
3098            verb: template.verb,
3099            roles: self.ctx.roles.alloc_slice(roles),
3100            modifiers: self.ctx.syms.alloc_slice(template.modifiers.clone()),
3101            suppress_existential,
3102            world: None,
3103        })));
3104
3105        // Apply modal if auxiliary is modal
3106        let with_modal = match aux_token {
3107            TokenType::Can | TokenType::Could => {
3108                let vector = self.token_to_vector(&aux_token);
3109                self.ctx.modal(vector, neo_event)
3110            }
3111            TokenType::Would | TokenType::May | TokenType::Must | TokenType::Should => {
3112                let vector = self.token_to_vector(&aux_token);
3113                self.ctx.modal(vector, neo_event)
3114            }
3115            _ => neo_event,
3116        };
3117
3118        // Apply negation if present
3119        let result = if is_negated {
3120            self.ctx.exprs.alloc(LogicExpr::UnaryOp {
3121                op: TokenType::Not,
3122                operand: with_modal,
3123            })
3124        } else {
3125            with_modal
3126        };
3127
3128        Some(Ok(result))
3129    }
3130
3131    fn try_parse_of_pair_xor(&mut self) -> ParseResult<Option<&'a LogicExpr<'a>>> {
3132        let start = self.current;
3133
3134        // Must start with the preposition "of"
3135        let is_of = {
3136            let text = self.interner.resolve(self.peek().lexeme).to_lowercase();
3137            text == "of" && matches!(self.peek().kind, TokenType::Preposition(_))
3138        };
3139        if !is_of {
3140            return Ok(None);
3141        }
3142
3143        // Scan for the structural boundaries: the comma ending "Of NP₁ and NP₂,"
3144        // and the last "and" before it (the NP₁/NP₂ separator). These give a
3145        // robust fallback when full-NP parsing misaligns with the boundary.
3146        let scan_start = self.current + 1; // one past "of"
3147        let comma_pos = {
3148            let mut found = None;
3149            for i in scan_start..self.tokens.len() {
3150                match &self.tokens[i].kind {
3151                    TokenType::Period | TokenType::EOF => break,
3152                    TokenType::Comma => {
3153                        found = Some(i);
3154                        break;
3155                    }
3156                    _ => {}
3157                }
3158            }
3159            match found {
3160                Some(p) => p,
3161                None => return Ok(None),
3162            }
3163        };
3164        let and_pos = {
3165            let mut found = None;
3166            for i in scan_start..comma_pos {
3167                if self.interner.resolve(self.tokens[i].lexeme).to_lowercase() == "and" {
3168                    found = Some(i);
3169                }
3170            }
3171            match found {
3172                Some(p) => p,
3173                None => return Ok(None),
3174            }
3175        };
3176        let one_ok = self
3177            .tokens
3178            .get(comma_pos + 1)
3179            .map(|t| self.interner.resolve(t.lexeme).to_lowercase() == "one")
3180            .unwrap_or(false);
3181        if !one_ok {
3182            return Ok(None);
3183        }
3184
3185        // Last content head in [lo, hi) (compounding a preceding cardinal) — the
3186        // robust fallback when parse_noun_phrase misaligns. A verb-only word
3187        // ("stamp", "print") or an ambiguous noun/verb token can head a puzzle NP
3188        // too, so they count as heads here; the fallback yields a bare constant
3189        // (its modifiers are lost — only reached when the full parse failed).
3190        fn scan_head<'a, 'ctx, 'int>(p: &mut Parser<'a, 'ctx, 'int>, lo: usize, hi: usize) -> Option<Symbol> {
3191            let mut i = hi;
3192            while i > lo {
3193                i -= 1;
3194                let head = match p.tokens[i].kind {
3195                    TokenType::ProperName(s) | TokenType::Noun(s) => Some(s),
3196                    TokenType::Verb { lemma, .. } => Some(lemma),
3197                    TokenType::Ambiguous { ref primary, ref alternatives } => {
3198                        match **primary {
3199                            TokenType::Noun(s) | TokenType::ProperName(s) => Some(s),
3200                            TokenType::Verb { lemma, .. } => Some(lemma),
3201                            _ => alternatives.iter().find_map(|t| match t {
3202                                TokenType::Noun(s) | TokenType::ProperName(s) => Some(*s),
3203                                TokenType::Verb { lemma, .. } => Some(*lemma),
3204                                _ => None,
3205                            }),
3206                        }
3207                    }
3208                    _ => None,
3209                };
3210                if let Some(s) = head {
3211                    if i > lo {
3212                        if let TokenType::Cardinal(n) = p.tokens[i - 1].kind {
3213                            return Some(
3214                                p.interner
3215                                    .intern(&format!("{}_{}", n, p.interner.resolve(s))),
3216                            );
3217                        }
3218                    }
3219                    return Some(s);
3220                }
3221            }
3222            None
3223        }
3224
3225        // Build one side of the pair, parsing the full NP for correctness but
3226        // committing only if it lands exactly at `boundary` without erroring;
3227        // otherwise fall back to the robust scan head (a bare constant). A
3228        // descriptive NP (determiner / adjective / possessor / PP / relative
3229        // clause) becomes a fresh existential variable carrying a restrictor so
3230        // two NPs sharing a head noun stay distinct; a bare proper name stays a
3231        // referring constant. Parsing is non-fatal so a malformed NP never breaks
3232        // an otherwise-valid clue.
3233        fn build_entity<'a, 'ctx, 'int>(
3234            p: &mut Parser<'a, 'ctx, 'int>,
3235            boundary: usize,
3236        ) -> ParseResult<OfEntity<'a>> {
3237            // An of-pair member is a nominal description ("the skydiving trip"),
3238            // so a verb-ambiguous head ("trip", "place") folds its modifier into
3239            // the head noun instead of being read as a verb — without this the NP
3240            // misaligns at the following "and" and the lossy scan_head fallback
3241            // drops the modifier ("the skydiving trip" → bare `Trip`).
3242            let saved_ctx = p.nominal_np_context;
3243            p.nominal_np_context = true;
3244            let np_result = p.parse_noun_phrase(true);
3245            p.nominal_np_context = saved_ctx;
3246            let np = np_result?;
3247            // who/that/where/whose relative, or a REDUCED relative ("the island first
3248            // seen by Captain Norris", "the well cut through chalk") — both restrict
3249            // the member and run up to the structural boundary ("and"/comma).
3250            let has_rel = (p.check(&TokenType::Who)
3251                || p.check(&TokenType::That)
3252                || p.check(&TokenType::Where)
3253                || p.check(&TokenType::Whose))
3254                && p.current < boundary;
3255            let has_reduced = p.peek_heads_reduced_relative_participle() && p.current < boundary;
3256            let is_desc = np.definiteness.is_some()
3257                || !np.adjectives.is_empty()
3258                || np.possessor.is_some()
3259                || !np.pps.is_empty()
3260                || has_rel
3261                || has_reduced;
3262            let (sym, is_var) = if is_desc {
3263                (p.next_var_name(), true)
3264            } else {
3265                (np.noun, false)
3266            };
3267            let term = if is_var {
3268                Term::Variable(sym)
3269            } else {
3270                Term::Constant(sym)
3271            };
3272            let rel = if has_rel {
3273                p.try_attach_relative(term)?
3274            } else {
3275                None
3276            };
3277            let reduced = if has_reduced {
3278                p.try_consume_reduced_relative(term)?
3279            } else {
3280                None
3281            };
3282            if p.current != boundary {
3283                return Err(ParseError {
3284                    kind: ParseErrorKind::Custom("of-pair NP misaligned".into()),
3285                    span: p.current_span(),
3286                });
3287            }
3288            let restrictor = if is_var {
3289                // Head noun + adjectives + possessor, all over the fresh variable.
3290                let mut r = p.nominal_predication(term, &np);
3291                for pp in np.pps {
3292                    let pp_sub = p.substitute_pp_placeholder(pp, sym);
3293                    r = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
3294                        left: r,
3295                        op: TokenType::And,
3296                        right: pp_sub,
3297                    });
3298                }
3299                for rc in rel.into_iter().chain(reduced) {
3300                    r = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
3301                        left: r,
3302                        op: TokenType::And,
3303                        right: rc,
3304                    });
3305                }
3306                Some(r)
3307            } else {
3308                None
3309            };
3310            Ok(OfEntity { sym, is_var, term, restrictor })
3311        }
3312
3313        self.advance(); // consume "of"
3314
3315        // NP₁, bounded by the "and".
3316        let e1 = match self.try_parse(|p| build_entity(p, and_pos)) {
3317            Some(e) => e,
3318            None => {
3319                self.current = and_pos;
3320                match scan_head(self, scan_start, and_pos) {
3321                    Some(h) => OfEntity { sym: h, is_var: false, term: Term::Constant(h), restrictor: None },
3322                    None => {
3323                        self.current = start;
3324                        return Ok(None);
3325                    }
3326                }
3327            }
3328        };
3329        self.advance(); // "and" separating NP₁ from NP₂
3330
3331        // NP₂, bounded by the comma.
3332        let e2 = match self.try_parse(|p| build_entity(p, comma_pos)) {
3333            Some(e) => e,
3334            None => {
3335                self.current = comma_pos;
3336                match scan_head(self, and_pos + 1, comma_pos) {
3337                    Some(h) => OfEntity { sym: h, is_var: false, term: Term::Constant(h), restrictor: None },
3338                    None => {
3339                        self.current = start;
3340                        return Ok(None);
3341                    }
3342                }
3343            }
3344        };
3345        self.advance(); // ","
3346        self.advance(); // "one" (validated above)
3347        // "one TYPE is …" / "one PERSON did …" — a redundant classifier noun
3348        // after "one" (the of-pair already binds the entity); skip it so VP₁
3349        // starts at the real predicate. Only when a Noun is directly followed by
3350        // a copula / auxiliary / verb (the predicate), never a bare object NP.
3351        {
3352            // A classifier is a generic noun ("one PERSON …") or a noun-or-verb
3353            // ambiguous word the lexicon also reads as a verb ("one TYPE …", "one
3354            // KIND …") — matched by lexeme so the verb tagging doesn't hide it. A
3355            // genuine aspectual verb ("one STARTED running") is never a classifier.
3356            let cur_is_classifier = matches!(
3357                self.tokens.get(self.current).map(|t| &t.kind),
3358                Some(TokenType::Noun(_))
3359            ) || self
3360                .tokens
3361                .get(self.current)
3362                .map(|t| {
3363                    matches!(
3364                        self.interner.resolve(t.lexeme).to_lowercase().as_str(),
3365                        "type" | "kind" | "sort" | "variety" | "category" | "version"
3366                    )
3367                })
3368                .unwrap_or(false);
3369            if cur_is_classifier
3370                && matches!(
3371                    self.tokens.get(self.current + 1).map(|t| &t.kind),
3372                    Some(TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
3373                        | TokenType::Verb { .. } | TokenType::Auxiliary(_))
3374                )
3375            {
3376                self.advance(); // skip the classifier noun
3377            }
3378        }
3379
3380        // Bound VP₁ at the "and the other" marker so a verb-object VP ("one teaches
3381        // yoga and the other is …") does not swallow "and the other …" as
3382        // coordinated objects. Scan for And + (article) + "other" and temporarily
3383        // turn that "and" into a clause terminator (a Period the VP parser stops
3384        // at); it is RESTORED on every exit path (errors, misalign, success).
3385        let other_and_pos = {
3386            let mut found = None;
3387            let mut i = self.current;
3388            while i + 2 < self.tokens.len() {
3389                if matches!(self.tokens[i].kind, TokenType::Period | TokenType::EOF) {
3390                    break;
3391                }
3392                if matches!(self.tokens[i].kind, TokenType::And)
3393                    && matches!(self.tokens[i + 1].kind, TokenType::Article(_))
3394                    && self
3395                        .interner
3396                        .resolve(self.tokens[i + 2].lexeme)
3397                        .eq_ignore_ascii_case("other")
3398                {
3399                    found = Some(i);
3400                    break;
3401                }
3402                i += 1;
3403            }
3404            found
3405        };
3406        let saved_and_tok = other_and_pos.map(|p| self.tokens[p].clone());
3407        if let Some(p) = other_and_pos {
3408            let mut t = self.tokens[p].clone();
3409            t.kind = TokenType::Period;
3410            self.tokens[p] = t;
3411        }
3412
3413        macro_rules! restore_and {
3414            () => {
3415                if let (Some(p), Some(tok)) = (other_and_pos, saved_and_tok.as_ref()) {
3416                    self.tokens[p] = tok.clone();
3417                }
3418            };
3419        }
3420
3421        // A verb phrase parsed from `start`, with `e` as its subject — restoring the
3422        // bounded "and" before propagating any parse error.
3423        macro_rules! vp_with {
3424            ($start:expr, $e:expr) => {{
3425                self.current = $start;
3426                let __r = if $e.is_var {
3427                    self.parse_predicate_with_subject_as_var($e.sym)
3428                } else {
3429                    self.parse_predicate_with_subject($e.sym)
3430                };
3431                match __r {
3432                    Ok(v) => v,
3433                    Err(err) => {
3434                        restore_and!();
3435                        return Err(err);
3436                    }
3437                }
3438            }};
3439        }
3440
3441        // VP₁ with e1 as subject (stops at the bounded marker).
3442        let vp1_start = self.current;
3443        let vp1_e1 = vp_with!(vp1_start, e1);
3444
3445        // Expect the "and the other" marker. When bounded, VP₁ stopped at the
3446        // Period that replaced "and"; an optional comma may precede it.
3447        if self.check(&TokenType::Comma) { self.advance(); }
3448        if let Some(marker) = other_and_pos {
3449            if self.current != marker || !self.check(&TokenType::Period) {
3450                restore_and!();
3451                self.current = start;
3452                return Ok(None);
3453            }
3454            self.advance(); // the bounded "and"
3455        } else {
3456            if self.interner.resolve(self.peek().lexeme).to_lowercase() != "and" {
3457                self.current = start;
3458                return Ok(None);
3459            }
3460            self.advance(); // consume "and"
3461        }
3462
3463        // Expect "the"
3464        if !self.check_article() {
3465            restore_and!();
3466            self.current = start;
3467            return Ok(None);
3468        }
3469        self.advance(); // consume "the"
3470
3471        // Expect "other"
3472        if self.interner.resolve(self.peek().lexeme).to_lowercase() != "other" {
3473            restore_and!();
3474            self.current = start;
3475            return Ok(None);
3476        }
3477        self.advance(); // consume "other"
3478        // "the other TYPE is …" — skip the redundant classifier noun (mirrors the
3479        // "one TYPE" skip above) so VP₂ starts at the real predicate.
3480        {
3481            // A classifier is a generic noun ("one PERSON …") or a noun-or-verb
3482            // ambiguous word the lexicon also reads as a verb ("one TYPE …", "one
3483            // KIND …") — matched by lexeme so the verb tagging doesn't hide it. A
3484            // genuine aspectual verb ("one STARTED running") is never a classifier.
3485            let cur_is_classifier = matches!(
3486                self.tokens.get(self.current).map(|t| &t.kind),
3487                Some(TokenType::Noun(_))
3488            ) || self
3489                .tokens
3490                .get(self.current)
3491                .map(|t| {
3492                    matches!(
3493                        self.interner.resolve(t.lexeme).to_lowercase().as_str(),
3494                        "type" | "kind" | "sort" | "variety" | "category" | "version"
3495                    )
3496                })
3497                .unwrap_or(false);
3498            if cur_is_classifier
3499                && matches!(
3500                    self.tokens.get(self.current + 1).map(|t| &t.kind),
3501                    Some(TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
3502                        | TokenType::Verb { .. } | TokenType::Auxiliary(_))
3503                )
3504            {
3505                self.advance(); // skip the classifier noun
3506            }
3507        }
3508
3509        // VP₂ with e2 as subject, then re-parse each VP with the other entity.
3510        let vp2_start = self.current;
3511        let vp2_e2 = vp_with!(vp2_start, e2);
3512        let end_pos = self.current;
3513        let vp1_e2 = vp_with!(vp1_start, e2);
3514        let vp2_e1 = vp_with!(vp2_start, e1);
3515        self.current = end_pos;
3516
3517        // All VP parses done — the bounded "and" is no longer needed; restore it
3518        // before building the result so the token stream is left pristine.
3519        restore_and!();
3520
3521        // Build: (VP₁(e1) ∧ VP₂(e2)) ∨ (VP₁(e2) ∧ VP₂(e1))
3522        let branch1 = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3523            left: vp1_e1,
3524            op: TokenType::And,
3525            right: vp2_e2,
3526        });
3527        let branch2 = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3528            left: vp1_e2,
3529            op: TokenType::And,
3530            right: vp2_e1,
3531        });
3532        let mut result: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3533            left: branch1,
3534            op: TokenType::Or,
3535            right: branch2,
3536        });
3537
3538        // "Of A and B" presents two DISTINCT entities. A variable entity could
3539        // otherwise co-refer with the other side and collapse the XOR, so assert
3540        // the inequality whenever either side is a variable; two proper-name
3541        // constants are distinct by the unique-name assumption already.
3542        if e1.is_var || e2.is_var {
3543            let identity = self.ctx.exprs.alloc(LogicExpr::Identity {
3544                left: self.ctx.terms.alloc(e1.term),
3545                right: self.ctx.terms.alloc(e2.term),
3546            });
3547            let ineq = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
3548                op: TokenType::Not,
3549                operand: identity,
3550            });
3551            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3552                left: ineq,
3553                op: TokenType::And,
3554                right: result,
3555            });
3556        }
3557
3558        // A description's restrictor (head noun, adjectives, possessor, PPs,
3559        // relative clause) holds in BOTH XOR branches, so it conjoins outside the
3560        // disjunction and is bound by the existential opened below.
3561        if let Some(r2) = e2.restrictor {
3562            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3563                left: r2,
3564                op: TokenType::And,
3565                right: result,
3566            });
3567        }
3568        if let Some(r1) = e1.restrictor {
3569            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3570                left: r1,
3571                op: TokenType::And,
3572                right: result,
3573            });
3574        }
3575        if e2.is_var {
3576            result = self.ctx.exprs.alloc(LogicExpr::Quantifier {
3577                kind: QuantifierKind::Existential,
3578                variable: e2.sym,
3579                body: result,
3580                island_id: self.current_island,
3581            });
3582        }
3583        if e1.is_var {
3584            result = self.ctx.exprs.alloc(LogicExpr::Quantifier {
3585                kind: QuantifierKind::Existential,
3586                variable: e1.sym,
3587                body: result,
3588                island_id: self.current_island,
3589            });
3590        }
3591        Ok(Some(result))
3592    }
3593}
3594
3595// Phase 46: Helper methods for generalized gapping (not part of trait)
3596impl<'a, 'ctx, 'int> Parser<'a, 'ctx, 'int> {
3597    /// Helper to extract verb specifically from NeoEvent structures
3598    fn extract_neo_event_verb(&self, expr: &LogicExpr<'a>) -> Option<Symbol> {
3599        match expr {
3600            LogicExpr::NeoEvent(data) => Some(data.verb),
3601            LogicExpr::Quantifier { body, .. } => self.extract_neo_event_verb(body),
3602            LogicExpr::BinaryOp { left, right, .. } => {
3603                self.extract_neo_event_verb(left)
3604                    .or_else(|| self.extract_neo_event_verb(right))
3605            }
3606            LogicExpr::Temporal { body, .. } => self.extract_neo_event_verb(body),
3607            LogicExpr::Aspectual { body, .. } => self.extract_neo_event_verb(body),
3608            _ => None,
3609        }
3610    }
3611
3612    /// Build roles for gapped clause using template guidance.
3613    /// NP args map to Theme/Recipient roles, PP args map by preposition type.
3614    fn build_gapped_roles(
3615        &self,
3616        subject_term: Term<'a>,
3617        np_args: &[Term<'a>],
3618        pp_args: &[(Symbol, Term<'a>)],
3619        template: &Option<EventTemplate<'a>>,
3620    ) -> Vec<(ThematicRole, Term<'a>)> {
3621        // Agent-gapping (non-constituent coordination, §2.1): "John gave Mary a book
3622        // and Sue a pen" — the remnant (Sue, a pen) fills the template's non-agent
3623        // NP roles (Recipient, Theme) IN ORDER and the agent is SHARED from the
3624        // template (John). Detected when the remnant count (subject + np_args) equals
3625        // the number of non-agent NP roles (≥ 2), i.e. the agent is the gap.
3626        if let Some(tmpl) = template {
3627            if let Some(shared_agent) = &tmpl.agent {
3628                let np_template_roles: Vec<_> = tmpl
3629                    .non_agent_roles
3630                    .iter()
3631                    .filter(|(r, _)| {
3632                        matches!(
3633                            r,
3634                            ThematicRole::Theme | ThematicRole::Recipient | ThematicRole::Patient
3635                        )
3636                    })
3637                    .collect();
3638                // A LONE bare-NP remnant after a complete transitive clause is
3639                // object coordination ("John saw himself and Mary" — Mary is
3640                // a second THEME, the agent is shared), not a new agent
3641                // inheriting the template's object.
3642                if np_template_roles.len() == 1 && np_args.is_empty() && pp_args.is_empty() {
3643                    let (role, _) = np_template_roles[0];
3644                    return vec![
3645                        (ThematicRole::Agent, shared_agent.clone()),
3646                        (*role, subject_term),
3647                    ];
3648                }
3649                if np_template_roles.len() >= 2 && 1 + np_args.len() == np_template_roles.len() {
3650                    let mut roles = vec![(ThematicRole::Agent, shared_agent.clone())];
3651                    let remnants: Vec<Term<'a>> = std::iter::once(subject_term)
3652                        .chain(np_args.iter().cloned())
3653                        .collect();
3654                    for ((role, _), arg) in np_template_roles.iter().zip(remnants.iter()) {
3655                        roles.push((*role, arg.clone()));
3656                    }
3657                    // Inherit template PP roles when none are overt in the remnant.
3658                    if pp_args.is_empty() {
3659                        for (role, term) in tmpl.non_agent_roles.iter().filter(|(r, _)| {
3660                            matches!(
3661                                r,
3662                                ThematicRole::Goal
3663                                    | ThematicRole::Source
3664                                    | ThematicRole::Location
3665                                    | ThematicRole::Instrument
3666                            )
3667                        }) {
3668                            roles.push((*role, term.clone()));
3669                        }
3670                    } else {
3671                        for (prep, term) in pp_args {
3672                            roles.push((self.preposition_to_role(*prep), term.clone()));
3673                        }
3674                    }
3675                    return roles;
3676                }
3677            }
3678        }
3679
3680        let mut roles = vec![(ThematicRole::Agent, subject_term)];
3681
3682        match template {
3683            Some(tmpl) => {
3684                let template_roles = &tmpl.non_agent_roles;
3685
3686                // Separate template roles into NP-type and PP-type
3687                let np_template_roles: Vec<_> = template_roles
3688                    .iter()
3689                    .filter(|(r, _)| {
3690                        matches!(
3691                            r,
3692                            ThematicRole::Theme | ThematicRole::Recipient | ThematicRole::Patient
3693                        )
3694                    })
3695                    .collect();
3696
3697                let pp_template_roles: Vec<_> = template_roles
3698                    .iter()
3699                    .filter(|(r, _)| {
3700                        matches!(
3701                            r,
3702                            ThematicRole::Goal
3703                                | ThematicRole::Source
3704                                | ThematicRole::Location
3705                                | ThematicRole::Instrument
3706                        )
3707                    })
3708                    .collect();
3709
3710                // Handle NPs by matching to template NP roles
3711                match (np_template_roles.len(), np_args.len()) {
3712                    (0, 0) => {} // Intransitive - no NP roles
3713                    (_, 0) => {
3714                        // Use all template NP roles unchanged
3715                        for (role, term) in &np_template_roles {
3716                            roles.push((*role, term.clone()));
3717                        }
3718                    }
3719                    (n, 1) if n > 0 => {
3720                        // 1 NP arg: replace LAST NP role (usually Theme), keep others
3721                        for (role, term) in np_template_roles.iter().take(n - 1) {
3722                            roles.push((*role, term.clone()));
3723                        }
3724                        if let Some((last_role, _)) = np_template_roles.last() {
3725                            roles.push((*last_role, np_args[0].clone()));
3726                        }
3727                    }
3728                    (n, m) if m == n => {
3729                        // Same count: replace all NP roles in order
3730                        for ((role, _), arg) in np_template_roles.iter().zip(np_args.iter()) {
3731                            roles.push((*role, arg.clone()));
3732                        }
3733                    }
3734                    (_, _) => {
3735                        // Fallback: assign Theme to each NP
3736                        for (i, arg) in np_args.iter().enumerate() {
3737                            let role = np_template_roles
3738                                .get(i)
3739                                .map(|(r, _)| *r)
3740                                .unwrap_or(ThematicRole::Theme);
3741                            roles.push((role, arg.clone()));
3742                        }
3743                    }
3744                }
3745
3746                // Handle PPs: use parsed PPs if provided, else use template
3747                if pp_args.is_empty() {
3748                    // Use template PP roles unchanged
3749                    for (role, term) in &pp_template_roles {
3750                        roles.push((*role, term.clone()));
3751                    }
3752                } else {
3753                    // Use parsed PPs, map preposition to role
3754                    for (prep, term) in pp_args {
3755                        let role = self.preposition_to_role(*prep);
3756                        roles.push((role, term.clone()));
3757                    }
3758                }
3759            }
3760            None => {
3761                // No template: backward-compat hardcoded Agent + Theme
3762                for arg in np_args {
3763                    roles.push((ThematicRole::Theme, arg.clone()));
3764                }
3765                for (prep, term) in pp_args {
3766                    let role = self.preposition_to_role(*prep);
3767                    roles.push((role, term.clone()));
3768                }
3769            }
3770        }
3771        roles
3772    }
3773
3774    /// Map preposition to thematic role
3775    fn preposition_to_role(&self, prep: Symbol) -> ThematicRole {
3776        let prep_str = self.interner.resolve(prep).to_lowercase();
3777        match prep_str.as_str() {
3778            "to" | "toward" | "towards" => ThematicRole::Goal,
3779            "from" => ThematicRole::Source,
3780            "in" | "on" | "at" => ThematicRole::Location,
3781            "with" | "by" => ThematicRole::Instrument,
3782            _ => ThematicRole::Location, // Default fallback
3783        }
3784    }
3785
3786    /// Check if modifier is temporal (for override filtering)
3787    fn is_temporal_modifier(&self, sym: Symbol) -> bool {
3788        let s = self.interner.resolve(sym).to_lowercase();
3789        matches!(
3790            s.as_str(),
3791            "yesterday" | "today" | "tomorrow" | "now" | "then" | "past" | "future"
3792        )
3793    }
3794}