Skip to main content

logicaffeine_language/parser/
verb.rs

1//! Verb phrase parsing with event semantics and thematic roles.
2//!
3//! This module handles the core verbal predication including:
4//!
5//! - **Intransitive verbs**: "John runs" → `∃e(Run(e) ∧ Agent(e,John))`
6//! - **Transitive verbs**: "John loves Mary" → `∃e(Love(e) ∧ Agent(e,John) ∧ Theme(e,Mary))`
7//! - **Ditransitive verbs**: "John gives Mary a book" → with Goal role
8//! - **Copula constructions**: "John is tall", "John is a doctor"
9//! - **Control verbs**: "John wants to run" → raising/control structures
10//! - **Plural subjects**: "John and Mary run", "John and Mary love each other"
11//! - **VP respectively**: "John and Mary love Sue and Bill respectively"
12//!
13//! # Neo-Davidsonian Event Semantics
14//!
15//! Verbs introduce event variables with thematic roles:
16//! - **Agent**: The doer of the action
17//! - **Theme/Patient**: The entity affected
18//! - **Goal/Recipient**: The target of transfer
19//! - **Instrument**: The tool used
20//!
21//! Events are represented using `LogicExpr::NeoEvent` with a verb symbol and
22//! a list of (ThematicRole, Term) pairs.
23
24use super::clause::ClauseParsing;
25use super::modal::ModalParsing;
26use super::noun::NounParsing;
27use super::pragmatics::PragmaticsParsing;
28use super::quantifier::QuantifierParsing;
29use super::{ParseResult, Parser};
30use crate::ast::{
31    AspectOperator, LogicExpr, NeoEventData, NounPhrase, QuantifierKind, TemporalOperator, Term,
32    ThematicRole,
33};
34use crate::drs::{Gender, Number, ReferentSource};
35use crate::error::{ParseError, ParseErrorKind};
36use logicaffeine_base::Symbol;
37use crate::lexer::Lexer;
38use crate::lexicon::{Aspect, Definiteness, Time};
39use crate::token::{FocusKind, Span, Token, TokenType};
40
41use crate::ast::Stmt;
42
43/// Trait for parsing verb phrases in declarative (logic) mode.
44///
45/// Provides methods for parsing predicates with subjects, control verbs,
46/// and plural/group constructions with Neo-Davidsonian event semantics.
47pub trait LogicVerbParsing<'a, 'ctx, 'int> {
48    /// Parses a verb phrase given the subject as a constant symbol.
49    fn parse_predicate_with_subject(&mut self, subject_symbol: Symbol)
50        -> ParseResult<&'a LogicExpr<'a>>;
51    /// Parses a verb phrase with subject as a bound variable.
52    fn parse_predicate_with_subject_as_var(&mut self, subject_symbol: Symbol)
53        -> ParseResult<&'a LogicExpr<'a>>;
54    /// Attempts to parse a plural subject ("John and Mary verb").
55    /// Returns `Ok(Some(expr))` on success, `Ok(None)` if not plural, `Err` on semantic error.
56    fn try_parse_plural_subject(&mut self, first_subject: &NounPhrase<'a>)
57        -> Result<Option<&'a LogicExpr<'a>>, ParseError>;
58    /// Parses control verb structures: "wants to VP", "persuaded X to VP".
59    fn parse_control_structure(
60        &mut self,
61        subject: &NounPhrase<'a>,
62        verb: Symbol,
63        verb_time: Time,
64    ) -> ParseResult<&'a LogicExpr<'a>>;
65    /// Checks if a verb is a control verb (want, try, persuade, etc.).
66    fn is_control_verb(&self, verb: Symbol) -> bool;
67    /// Builds a predicate for intransitive verbs with multiple subjects.
68    fn build_group_predicate(
69        &mut self,
70        subjects: &[Symbol],
71        verb: Symbol,
72        verb_time: Time,
73    ) -> &'a LogicExpr<'a>;
74    /// Builds a transitive predicate with group subject and group object.
75    fn build_group_transitive(
76        &mut self,
77        subjects: &[Symbol],
78        objects: &[Symbol],
79        verb: Symbol,
80        verb_time: Time,
81    ) -> &'a LogicExpr<'a>;
82}
83
84/// Trait for parsing verb phrases in imperative (LOGOS) mode.
85///
86/// Provides methods for parsing statements rather than logical propositions.
87pub trait ImperativeVerbParsing<'a, 'ctx, 'int> {
88    /// Parses a statement with the given subject symbol.
89    fn parse_statement_with_subject(&mut self, subject_symbol: Symbol)
90        -> ParseResult<Stmt<'a>>;
91}
92
93impl<'a, 'ctx, 'int> Parser<'a, 'ctx, 'int> {
94    /// Nominal copula predication: the body of "SUBJ is (the) PRED-NP".
95    ///
96    /// The subject is identified with the predicate nominal, so every property
97    /// of the predicate NP is predicated of the SUBJECT term:
98    /// `Pred(subj) ∧ Adj_i(subj) ∧ [Possesses(possessor, subj)]`, recursing
99    /// through the possessor's own possessor chain. This keeps the genitive
100    /// constraint — "X is the Alvarado family's house" entails the Alvarado
101    /// family possesses X — instead of dropping it.
102    pub(super) fn nominal_predication(
103        &mut self,
104        subject_term: Term<'a>,
105        pred_np: &NounPhrase<'a>,
106    ) -> &'a LogicExpr<'a> {
107        let mut result = self.ctx.exprs.alloc(LogicExpr::Predicate {
108            name: pred_np.noun,
109            args: self.ctx.terms.alloc_slice([subject_term]),
110            world: None,
111        });
112
113        for &adj in pred_np.adjectives {
114            let adj_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
115                name: adj,
116                args: self.ctx.terms.alloc_slice([subject_term]),
117                world: None,
118            });
119            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
120                left: result,
121                op: TokenType::And,
122                right: adj_pred,
123            });
124        }
125
126        if let Some(possessor) = pred_np.possessor {
127            let poss_logic = self.possessor_predication(possessor, subject_term);
128            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
129                left: result,
130                op: TokenType::And,
131                right: poss_logic,
132            });
133        }
134
135        result
136    }
137
138    /// THE single decision point for whether a possessor / passive-by-agent NP
139    /// refers by a bare constant or must become its own restrictor-carrying entity.
140    /// Returns the term that stands for the possessor/agent in the relation, plus
141    /// (when descriptive) the fresh variable and the restrictor that scopes it:
142    ///   - a **bare** NP (no adjectives, no nested genitive, no PPs — "Agnes",
143    ///     "His", "John's") → `(Term::Constant(np.noun), None)`, the referring
144    ///     constant, so its output is byte-identical to the historical form;
145    ///   - a **descriptive** NP ("the old captain", "the Woodard family", "the red
146    ///     team") → `(Term::Variable(pvar), Some((pvar, restrictor)))` where the
147    ///     restrictor is `nominal_predication(Variable(pvar), np)` conjoined with
148    ///     each of the NP's PPs (substituted onto `pvar`), so EVERY modifier
149    ///     survives instead of collapsing to the bare head.
150    ///
151    /// The restrictor recurses through [`Self::nominal_predication`] (which re-enters
152    /// [`Self::possessor_entity`] for a nested genitive), so an arbitrarily deep
153    /// "X's B's C" is handled uniformly. Callers wrap the relation they build over
154    /// the returned term with [`Self::wrap_in_possessor_entity`].
155    pub(super) fn possessor_entity(
156        &mut self,
157        np: &NounPhrase<'a>,
158    ) -> (Term<'a>, Option<(Symbol, &'a LogicExpr<'a>)>) {
159        let is_descriptive = !np.adjectives.is_empty()
160            || np.possessor.is_some()
161            || !np.pps.is_empty();
162        if !is_descriptive {
163            return (Term::Constant(np.noun), None);
164        }
165        let pvar = self.next_var_name();
166        let mut restr = self.nominal_predication(Term::Variable(pvar), np);
167        for pp in np.pps {
168            let pp_sub = self.substitute_pp_placeholder(pp, pvar);
169            restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
170                left: restr,
171                op: TokenType::And,
172                right: pp_sub,
173            });
174        }
175        (Term::Variable(pvar), Some((pvar, restr)))
176    }
177
178    /// Scope a relation built over a possessor/agent term inside that entity's
179    /// existential, when [`Self::possessor_entity`] produced a restrictor:
180    ///   - `None` (bare possessor/agent) → `relation` unchanged;
181    ///   - `Some((pvar, restrictor))` → `∃pvar(restrictor ∧ relation)`.
182    pub(super) fn wrap_in_possessor_entity(
183        &mut self,
184        restr: Option<(Symbol, &'a LogicExpr<'a>)>,
185        relation: &'a LogicExpr<'a>,
186    ) -> &'a LogicExpr<'a> {
187        match restr {
188            None => relation,
189            Some((pvar, restrictor)) => {
190                let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
191                    left: restrictor,
192                    op: TokenType::And,
193                    right: relation,
194                });
195                self.ctx.exprs.alloc(LogicExpr::Quantifier {
196                    kind: QuantifierKind::Existential,
197                    variable: pvar,
198                    body,
199                    island_id: self.current_island,
200                })
201            }
202        }
203    }
204
205    /// Lower a possessor noun phrase to its logical contribution, predicating it of
206    /// `possessed_term`. THE single place a genitive becomes logic, so a possessor's
207    /// adjectives / nested genitive / PPs survive in EVERY syntactic position rather
208    /// than each construction re-deriving (and dropping) them:
209    ///   - a **descriptive** possessor ("the old captain", "X's B") → its own
210    ///     existential entity carrying its full restrictor, recursively lowered:
211    ///     `∃p(Restrictor(p) ∧ Possesses(p, possessed))`;
212    ///   - a **bare** proper name ("Agnes") → the referring constant:
213    ///     `Possesses(Agnes, possessed)`.
214    pub(super) fn possessor_predication(
215        &mut self,
216        possessor: &NounPhrase<'a>,
217        possessed_term: Term<'a>,
218    ) -> &'a LogicExpr<'a> {
219        let (poss_term, restr) = self.possessor_entity(possessor);
220        let possesses = self.ctx.exprs.alloc(LogicExpr::Predicate {
221            name: self.interner.intern("Possesses"),
222            args: self.ctx.terms.alloc_slice([poss_term, possessed_term]),
223            world: None,
224        });
225        self.wrap_in_possessor_entity(restr, possesses)
226    }
227
228    /// Like [`Self::nominal_predication`] but ALSO conjoins the NP's PPs / reduced
229    /// relatives (the `_PP_SELF_` placeholder substituted to `subject_term`,
230    /// whether a constant or a variable). Use at sites that predicate a full NP of
231    /// a term and would otherwise drop "the medicine sourced from a fig" down to
232    /// `Medicine(x)`. Does NOT double-count at sites that already loop the PPs.
233    pub(super) fn nominal_predication_with_pps(
234        &mut self,
235        subject_term: Term<'a>,
236        pred_np: &NounPhrase<'a>,
237    ) -> &'a LogicExpr<'a> {
238        let mut result = self.nominal_predication(subject_term, pred_np);
239        for pp in pred_np.pps {
240            // Recurse through the restrictor's structure so a COMPLEX pp — a reduced
241            // relative built as a NeoEvent / quantified conjunction, not just a flat
242            // Predicate — has its `_PP_SELF_` gap bound and is NOT silently dropped.
243            let pp_sub = self.substitute_pp_self_term(pp, subject_term);
244            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
245                left: result,
246                op: TokenType::And,
247                right: pp_sub,
248            });
249        }
250        result
251    }
252
253    /// THE single place a PP object's own modifiers are recovered: its ADJECTIVES
254    /// ("the BIG table") and nested Predicate PPs ("a range OF 650 ft"), each
255    /// predicated of the object constant (nested PPs' `_PP_SELF_` rebound to it).
256    /// Every PP position (NP-internal, copula, passive-agent, modal) routes through
257    /// this so "on the BIG table" / "by the LOCAL police" never drop the modifier.
258    pub(super) fn pp_object_modifier_preds(
259        &mut self,
260        pp_object: &NounPhrase<'a>,
261    ) -> Vec<&'a LogicExpr<'a>> {
262        let mut out: Vec<&'a LogicExpr<'a>> = Vec::new();
263        for &adj in pp_object.adjectives {
264            out.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
265                name: adj,
266                args: self.ctx.terms.alloc_slice([Term::Constant(pp_object.noun)]),
267                world: None,
268            }));
269        }
270        let placeholder = self.interner.intern("_PP_SELF_");
271        for nested in pp_object.pps {
272            if let LogicExpr::Predicate { name, args, world } = nested {
273                let new_args: Vec<Term<'a>> = args
274                    .iter()
275                    .map(|a| match a {
276                        Term::Variable(v) if *v == placeholder => Term::Constant(pp_object.noun),
277                        other => *other,
278                    })
279                    .collect();
280                out.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
281                    name: *name,
282                    args: self.ctx.terms.alloc_slice(new_args),
283                    world: *world,
284                }));
285            }
286        }
287        out
288    }
289
290    /// Conjoin a PP object's recovered modifiers (see [`Self::pp_object_modifier_preds`])
291    /// onto `pred` — for PP sites that build a single conjoined relation (copula,
292    /// passive-agent, modal) rather than a `pps` list.
293    pub(super) fn attach_pp_object_modifiers(
294        &mut self,
295        mut pred: &'a LogicExpr<'a>,
296        pp_object: &NounPhrase<'a>,
297    ) -> &'a LogicExpr<'a> {
298        for m in self.pp_object_modifier_preds(pp_object) {
299            pred = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
300                left: pred,
301                op: TokenType::And,
302                right: m,
303            });
304        }
305        pred
306    }
307
308    /// Consume trailing EVENT PP-adjuncts ("takes a holiday WITH a friend TO some
309    /// location") and conjoin each as `Prep(event, obj)` onto `event`. The
310    /// non-quantified object path consumes these inline in its main PP loop; the
311    /// QUANTIFIED-object path wraps the event in `∃`, so without this the PPs strand
312    /// as trailing tokens. A bare preposition with no object is handed back for the
313    /// sentence-level parse to report rather than silently dropped.
314    pub(super) fn attach_trailing_event_pps(
315        &mut self,
316        mut event: &'a LogicExpr<'a>,
317        event_var: Symbol,
318    ) -> ParseResult<&'a LogicExpr<'a>> {
319        while self.check_preposition() || self.check_to() {
320            // "within N <unit>" is a temporal bound, not a PP.
321            if self.check_preposition_is("within")
322                && matches!(
323                    self.tokens.get(self.current + 1).map(|t| &t.kind),
324                    Some(TokenType::Cardinal(_)) | Some(TokenType::Number(_))
325                )
326            {
327                break;
328            }
329            // An NP object must follow; otherwise leave the preposition in place.
330            let object_follows = matches!(
331                self.tokens.get(self.current + 1).map(|t| &t.kind),
332                Some(TokenType::Article(_))
333                    | Some(TokenType::Noun(_))
334                    | Some(TokenType::ProperName(_))
335                    | Some(TokenType::All)
336                    | Some(TokenType::Some)
337                    | Some(TokenType::Any)
338                    | Some(TokenType::Cardinal(_))
339                    | Some(TokenType::Number(_))
340                    | Some(TokenType::Pronoun { .. })
341            );
342            if !object_follows {
343                break;
344            }
345            let prep_token = self.advance().clone();
346            let prep_name = match prep_token.kind {
347                TokenType::Preposition(sym) => sym,
348                TokenType::To => self.interner.intern("To"),
349                _ => break,
350            };
351            // A quantified PP object ("to SOME new location", "with ANY friend")
352            // leads with a bare quantifier token that parse_noun_phrase does not
353            // consume; drop it so the head noun reads as the object referent. The
354            // PP object is represented by its noun constant, matching the indefinite
355            // ("with a friend" → With(e, Friend)) convention.
356            if matches!(
357                self.peek().kind,
358                TokenType::Some | TokenType::Any | TokenType::All | TokenType::No
359            ) {
360                self.advance();
361            }
362            let pp_np = self.parse_noun_phrase(false)?;
363            let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
364                name: prep_name,
365                args: self
366                    .ctx
367                    .terms
368                    .alloc_slice([Term::Variable(event_var), Term::Constant(pp_np.noun)]),
369                world: None,
370            });
371            event = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
372                left: event,
373                op: TokenType::And,
374                right: pp_pred,
375            });
376        }
377        Ok(event)
378    }
379
380    /// Substitute the `_PP_SELF_` placeholder with `term` (constant OR variable)
381    /// throughout a restrictor, recursing through connectives / quantifiers / events.
382    /// Generalizes [`QuantifierParsing::substitute_pp_placeholder`] (which targets a
383    /// variable) so a reduced-relative restrictor binds its gap wherever it sits.
384    pub(super) fn substitute_pp_self_term(
385        &mut self,
386        pp: &'a LogicExpr<'a>,
387        term: Term<'a>,
388    ) -> &'a LogicExpr<'a> {
389        let placeholder = self.interner.intern("_PP_SELF_");
390        match pp {
391            LogicExpr::Predicate { name, args, .. } => {
392                let new_args: Vec<Term<'a>> = args
393                    .iter()
394                    .map(|a| match a {
395                        Term::Variable(v) if *v == placeholder => term,
396                        other => *other,
397                    })
398                    .collect();
399                self.ctx.exprs.alloc(LogicExpr::Predicate {
400                    name: *name,
401                    args: self.ctx.terms.alloc_slice(new_args),
402                    world: None,
403                })
404            }
405            LogicExpr::BinaryOp { left, op, right } => {
406                let l = self.substitute_pp_self_term(left, term);
407                let r = self.substitute_pp_self_term(right, term);
408                self.ctx.exprs.alloc(LogicExpr::BinaryOp { left: l, op: op.clone(), right: r })
409            }
410            LogicExpr::UnaryOp { op, operand } => {
411                let o = self.substitute_pp_self_term(operand, term);
412                self.ctx.exprs.alloc(LogicExpr::UnaryOp { op: op.clone(), operand: o })
413            }
414            LogicExpr::Quantifier { kind, variable, body, island_id } => {
415                let b = self.substitute_pp_self_term(body, term);
416                self.ctx.exprs.alloc(LogicExpr::Quantifier {
417                    kind: *kind,
418                    variable: *variable,
419                    body: b,
420                    island_id: *island_id,
421                })
422            }
423            LogicExpr::Temporal { operator, body } => {
424                let b = self.substitute_pp_self_term(body, term);
425                self.ctx.exprs.alloc(LogicExpr::Temporal { operator: *operator, body: b })
426            }
427            LogicExpr::NeoEvent(data) => {
428                let new_roles: Vec<(ThematicRole, Term<'a>)> = data
429                    .roles
430                    .iter()
431                    .map(|(role, t)| {
432                        let nt = match t {
433                            Term::Variable(v) if *v == placeholder => term,
434                            other => *other,
435                        };
436                        (*role, nt)
437                    })
438                    .collect();
439                self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
440                    event_var: data.event_var,
441                    verb: data.verb,
442                    roles: self.ctx.roles.alloc_slice(new_roles),
443                    modifiers: data.modifiers,
444                    suppress_existential: data.suppress_existential,
445                    world: data.world,
446                })))
447            }
448            _ => pp,
449        }
450    }
451
452    /// If a relative clause ("WHO played", "THAT won 9 games") trails a predicate
453    /// nominal, conjoin it onto `pred`, predicated of the subject — "X is the
454    /// player who played" entails X played. The gap binds to the subject term
455    /// (constant or variable). A no-op when no relative clause follows. Shared by
456    /// every copula-complement site (plain `is the Y`, `either A or B`,
457    /// neither/nor, quantified subjects) so the feature composes once.
458    /// If a relativizer follows, parse the relative clause over `term` and return it.
459    /// One place so who/that (argument gap), where (locative), and whose (possessive)
460    /// compose UNIFORMLY at every NP attachment site (subject, predicate nominal,
461    /// either-or / of-pair member, comparison/temporal standard) instead of each site
462    /// re-checking who/that inline and silently stranding where/whose. The where/whose
463    /// helpers consume their own relativizer; parse_relative_clause expects who/that
464    /// already consumed (matching its existing callers).
465    pub(super) fn try_attach_relative(
466        &mut self,
467        term: Term<'a>,
468    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
469        if let Term::Constant(s) | Term::Variable(s) = term {
470            if self.check(&TokenType::Who) || self.check(&TokenType::That) {
471                self.advance();
472                return Ok(Some(self.parse_relative_clause(s)?));
473            } else if self.check(&TokenType::Where) {
474                return Ok(Some(self.parse_where_relative(s)?));
475            } else if self.check(&TokenType::Whose) {
476                return Ok(Some(self.parse_whose_relative(s)?));
477            }
478        }
479        Ok(None)
480    }
481
482    pub(super) fn conjoin_trailing_relative(
483        &mut self,
484        pred: &'a LogicExpr<'a>,
485        subject_term: Term<'a>,
486    ) -> ParseResult<&'a LogicExpr<'a>> {
487        let mut pred = pred;
488        // who/that/where/whose relative on the complement.
489        if let Some(rc) = self.try_attach_relative(subject_term.clone())? {
490            pred = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
491                left: pred,
492                op: TokenType::And,
493                right: rc,
494            });
495        }
496        // A REDUCED relative ("is the mountain FIRST CLIMBED in 1845", "is the island
497        // SEEN by Captain Norris") — every caller is a copula complement / predicate
498        // nominal / disjunct, where the copula is already consumed, so a trailing
499        // participle is a reduced relative restricting the subject, never a matrix verb.
500        if let Some(rr) = self.try_consume_reduced_relative(subject_term)? {
501            pred = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
502                left: pred,
503                op: TokenType::And,
504                right: rr,
505            });
506        }
507        Ok(pred)
508    }
509
510    /// Conjoin a temporal adverb consumed after a relative-clause copula ("who is NOW
511    /// with the Tigers") over the gap, or return the predication unchanged when there
512    /// was no adverb. Shared by the progressive and the predicate-complement exits of
513    /// the copular relative branch.
514    pub(super) fn conjoin_relative_temporal_adverb(
515        &mut self,
516        pred: &'a LogicExpr<'a>,
517        gap_var: Symbol,
518        adv: Option<Symbol>,
519    ) -> &'a LogicExpr<'a> {
520        match adv {
521            Some(a) => {
522                let adv_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
523                    name: a,
524                    args: self.ctx.terms.alloc_slice([Term::Variable(gap_var)]),
525                    world: None,
526                });
527                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
528                    left: pred,
529                    op: TokenType::And,
530                    right: adv_pred,
531                })
532            }
533            None => pred,
534        }
535    }
536
537    /// Copula complement led by a temporal/ordinal adverb: "was FIRST" (ranked
538    /// first), "is NOW the leader", "was THEN the champion". Returns the base
539    /// predication over `subject_term` — the adverb conjoined with any following
540    /// predicate nominal/adjective ("is now THE LEADER" → Leader(x) ∧ Now(x)), or the
541    /// bare adverb when it is the whole complement ("was first" → First(x)). Returns
542    /// None when the next token is not a temporal adverb, so the caller falls through
543    /// to its other complement branches. The caller applies its own tense / negation /
544    /// definiteness wrapping. Restricted to TemporalAdverb so a degree adverb ("is
545    /// very tall") is not mis-attached to the subject. Shared by parse_atom and
546    /// parse_predicate so both copula paths gain the reading from one place.
547    pub(super) fn copula_temporal_adverb_complement(
548        &mut self,
549        subject_term: Term<'a>,
550    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
551        let adv = match self.peek().kind {
552            TokenType::TemporalAdverb(s) => s,
553            _ => return Ok(None),
554        };
555        self.advance();
556        let adv_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
557            name: adv,
558            args: self.ctx.terms.alloc_slice([subject_term.clone()]),
559            world: None,
560        });
561        if self.check_article() || self.check_content_word() {
562            let saved_ctx = self.nominal_np_context;
563            self.nominal_np_context = true;
564            let comp_np_result = self.parse_noun_phrase(true);
565            self.nominal_np_context = saved_ctx;
566            let comp_np = comp_np_result?;
567            let comp_pred = self.nominal_predication_with_pps(subject_term.clone(), &comp_np);
568            let comp_pred = self.conjoin_trailing_relative(comp_pred, subject_term.clone())?;
569            return Ok(Some(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
570                left: comp_pred,
571                op: TokenType::And,
572                right: adv_pred,
573            })));
574        }
575        Ok(Some(adv_pred))
576    }
577
578    /// Conjoin an NP's restrictions — possessor (`Possesses(possessor, entity)`)
579    /// and PPs (with the `_PP_SELF_` placeholder substituted to the entity) —
580    /// onto a predication. Used wherever an NP is reduced to its head symbol
581    /// (comparative standards, disjuncts, list members) so the possessor / PP
582    /// constraints are not silently dropped — zero meaning loss.
583    pub(super) fn augment_with_np_restrictions(
584        &mut self,
585        expr: &'a LogicExpr<'a>,
586        np: &NounPhrase<'a>,
587    ) -> &'a LogicExpr<'a> {
588        let entity = Term::Constant(np.noun);
589        let mut result = expr;
590        if let Some(possessor) = np.possessor {
591            let possesses = self.interner.intern("Possesses");
592            let poss = self.ctx.exprs.alloc(LogicExpr::Predicate {
593                name: possesses,
594                args: self
595                    .ctx
596                    .terms
597                    .alloc_slice([Term::Constant(possessor.noun), entity]),
598                world: None,
599            });
600            result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
601                left: result,
602                op: TokenType::And,
603                right: poss,
604            });
605        }
606        if !np.pps.is_empty() {
607            let placeholder = self.interner.intern("_PP_SELF_");
608            for pp in np.pps {
609                let pp_sub = match pp {
610                    LogicExpr::Predicate { name, args, world } => {
611                        let new_args: Vec<Term<'a>> = args
612                            .iter()
613                            .map(|a| match a {
614                                Term::Variable(v) if *v == placeholder => entity,
615                                other => *other,
616                            })
617                            .collect();
618                        self.ctx.exprs.alloc(LogicExpr::Predicate {
619                            name: *name,
620                            args: self.ctx.terms.alloc_slice(new_args),
621                            world: *world,
622                        })
623                    }
624                    other => *other,
625                };
626                result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
627                    left: result,
628                    op: TokenType::And,
629                    right: pp_sub,
630                });
631            }
632        }
633        result
634    }
635
636    /// Apply the copula's tense, negation, and temporal-adverb wrappers to a
637    /// finished predication body, in the same order the bare-predicate path uses.
638    pub(super) fn finish_copula(
639        &self,
640        base: &'a LogicExpr<'a>,
641        copula_time: Time,
642        is_negated: bool,
643        copula_temporal: Option<super::CopulaTemporal>,
644    ) -> &'a LogicExpr<'a> {
645        let with_time = if copula_time == Time::Past {
646            self.ctx.exprs.alloc(LogicExpr::Temporal {
647                operator: TemporalOperator::Past,
648                body: base,
649            })
650        } else {
651            base
652        };
653        let with_neg = if is_negated {
654            self.ctx.exprs.alloc(LogicExpr::UnaryOp {
655                op: TokenType::Not,
656                operand: with_time,
657            })
658        } else {
659            with_time
660        };
661        match copula_temporal {
662            Some(super::CopulaTemporal::Always) => self.ctx.exprs.alloc(LogicExpr::Temporal {
663                operator: TemporalOperator::Always,
664                body: with_neg,
665            }),
666            Some(super::CopulaTemporal::Never) => {
667                let negated = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
668                    op: TokenType::Not,
669                    operand: with_time,
670                });
671                self.ctx.exprs.alloc(LogicExpr::Temporal {
672                    operator: TemporalOperator::Always,
673                    body: negated,
674                })
675            }
676            Some(super::CopulaTemporal::Eventually) => self.ctx.exprs.alloc(LogicExpr::Temporal {
677                operator: TemporalOperator::Eventually,
678                body: with_neg,
679            }),
680            None => with_neg,
681        }
682    }
683
684    /// Arithmetic / vague verbal comparative after a just-consumed verb.
685    ///
686    /// Matches `[MEASURE] [DEGREE] COMPARATIVE "than" STANDARD`, where the verb
687    /// names the graded dimension (its event is asserted), a measure phrase
688    /// ("3 points") is the exact offset, and a degree modifier ("somewhat")
689    /// marks a strict-but-imprecise inequality. The bare "faster than Bob" case
690    /// (no measure, no degree modifier) is left for the caller. Returns `None`
691    /// without consuming when the pattern is absent.
692    pub(super) fn try_arithmetic_comparative(
693        &mut self,
694        verb: Symbol,
695        subject_term: Term<'a>,
696        verb_time: Time,
697    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
698        let is_unit = |k: &TokenType| {
699            matches!(
700                k,
701                TokenType::Noun(_)
702                    | TokenType::Adjective(_)
703                    | TokenType::NonIntersectiveAdjective(_)
704                    | TokenType::Verb { .. }
705                    | TokenType::ProperName(_)
706                    | TokenType::Performative(_)
707                    | TokenType::Ambiguous { .. }
708                    // Measure-unit tokens ("10 more MINUTES than", "3 ounces more
709                    // gold than") — a calendar/clock unit or duration literal heads
710                    // the comparison dimension just like a plain unit noun.
711                    | TokenType::CalendarUnit(_)
712                    | TokenType::DurationLiteral { .. }
713            )
714        };
715        let cp = self.checkpoint();
716
717        // 0. A price/measure comparative may be introduced by "for"/"at"/"with"
718        // ("sold for $25,000 less than Y", "sold for somewhat less than Y",
719        // "finished with 3 ounces more gold than Y"). Tentatively drop it; the
720        // checkpoint restores it if no comparative follows, so a plain "sold for
721        // $105" / "finished with a medal" still flows to the PP handler.
722        if matches!(self.peek().kind, TokenType::Preposition(s)
723            if matches!(self.interner.resolve(s).to_lowercase().as_str(), "for" | "at" | "with"))
724        {
725            self.advance();
726        }
727
728        // 0b. A possessed-quality comparative is introduced by an indefinite
729        // article ("has a narrower wingspan than Y", "has a 4 inches narrower
730        // wingspan than Y"). Tentatively drop it; the checkpoint restores it if no
731        // comparative follows, so a plain "has a wingspan" still flows onward.
732        if matches!(self.peek().kind, TokenType::Article(crate::lexicon::Definiteness::Indefinite)) {
733            self.advance();
734        }
735
736        // 1. Optional numeric offset ("3", "190").
737        let count_kind: Option<crate::ast::NumberKind> = match self.peek().kind {
738            TokenType::Number(s) => {
739                self.advance();
740                let raw = self.interner.resolve(s);
741                Some(if let Ok(n) = raw.parse::<i64>() {
742                    crate::ast::NumberKind::Integer(n)
743                } else if raw.contains('.') {
744                    crate::ast::NumberKind::Real(raw.parse().unwrap_or(0.0))
745                } else {
746                    crate::ast::NumberKind::Symbolic(s)
747                })
748            }
749            TokenType::Cardinal(n) => {
750                self.advance();
751                Some(crate::ast::NumberKind::Integer(n as i64))
752            }
753            _ => None,
754        };
755
756        // 2. Optional unit noun BEFORE the comparative ("3 points lower").
757        let mut unit_sym: Option<Symbol> = None;
758        if count_kind.is_some()
759            && is_unit(&self.peek().kind)
760            && matches!(
761                self.tokens.get(self.current + 1).map(|t| &t.kind),
762                Some(TokenType::Comparative(_))
763            )
764        {
765            unit_sym = Some(self.consume_content_word()?);
766        }
767
768        // 3. A dimension noun and/or degree modifier between here and the comparative.
769        // A NOUN ("a wingspan LONGER than", "a face WIDER than") is the comparison
770        // DIMENSION → unit_sym, so the measure is Wingspan(x)/Wingspan(y) and not the
771        // verb (the prenominal form "a LONGER wingspan than" puts the comparative first
772        // and is captured at step 5). A non-noun degree word ("somewhat", "much") is
773        // discarded. Both may appear ("a wingspan SOMEWHAT longer than"). Only the
774        // count-less case — the numeric "4 inches narrower" path is handled at step 2.
775        let mut has_vague = false;
776        if unit_sym.is_none() && count_kind.is_none() {
777            // Pure lookahead (no checkpoint side effects): scan dimension noun(s), an
778            // optional degree adverb, and require a Comparative to follow. Only then
779            // consume — so a bare object ("won her prize") with no comparative is
780            // untouched.
781            let mut k = self.current;
782            while self.tokens.get(k).map_or(false, |t| {
783                matches!(t.kind, TokenType::Noun(_) | TokenType::Ambiguous { .. })
784                    && !crate::lexicon::is_degree_adverb(
785                        &self.interner.resolve(t.lexeme).to_lowercase(),
786                    )
787            }) {
788                k += 1;
789            }
790            let dim_end = k;
791            let has_dim = dim_end > self.current;
792            let degree = !matches!(
793                self.tokens.get(k).map(|t| &t.kind),
794                Some(TokenType::Comparative(_))
795            ) && matches!(
796                self.tokens.get(k + 1).map(|t| &t.kind),
797                Some(TokenType::Comparative(_))
798            );
799            let comp_at = if degree { k + 1 } else { k };
800            let comp_here = matches!(
801                self.tokens.get(comp_at).map(|t| &t.kind),
802                Some(TokenType::Comparative(_))
803            );
804            if has_dim && comp_here {
805                let mut dim: Option<Symbol> = None;
806                while self.current < dim_end {
807                    let n = self.consume_content_word()?;
808                    dim = Some(match dim {
809                        Some(d) => self.interner.intern(&format!(
810                            "{}_{}",
811                            self.interner.resolve(d),
812                            self.interner.resolve(n)
813                        )),
814                        None => n,
815                    });
816                }
817                if degree {
818                    self.advance(); // degree adverb (discarded)
819                    has_vague = true;
820                }
821                unit_sym = dim;
822            }
823        }
824        // 3b. A bare degree modifier with no dimension noun ("somewhat higher").
825        if unit_sym.is_none()
826            && !matches!(self.peek().kind, TokenType::Comparative(_))
827            && matches!(
828                self.tokens.get(self.current + 1).map(|t| &t.kind),
829                Some(TokenType::Comparative(_))
830            )
831        {
832            self.advance(); // degree modifier
833            has_vague = true;
834        }
835
836        // 4. The comparative itself.
837        let comp_adj = match self.peek().kind {
838            TokenType::Comparative(a) => {
839                self.advance();
840                a
841            }
842            _ => {
843                self.restore(cp);
844                return Ok(None);
845            }
846        };
847
848        // 5. A unit / dimension noun phrase AFTER the comparative, immediately before
849        // "than" ("1 more game than", "less baking time than", "received 7 votes more
850        // votes than Ken"). Consume the WHOLE (possibly multi-word) dimension up to
851        // "than"; join multi-word into one measure symbol ("baking time" → Baking_time)
852        // so the prover relates the same dimension. A unit already captured before the
853        // comparative makes a post-comparative noun redundant — consumed but discarded.
854        {
855            let mut k = self.current;
856            while self.tokens.get(k).map_or(false, |t| is_unit(&t.kind)) {
857                k += 1;
858            }
859            let unit_end = k;
860            // An infinitive purpose modifier on the dimension ("10 more minutes TO
861            // PRINT than") specifies what the measured amount is FOR; fold the verb
862            // into the dimension so the prover relates the same measure on both sides
863            // ("minutes to print" → Minute_print).
864            let infinitive: Option<Symbol> = if unit_end > self.current
865                && matches!(self.tokens.get(unit_end).map(|t| &t.kind), Some(TokenType::To))
866            {
867                match self.tokens.get(unit_end + 1).map(|t| t.kind.clone()) {
868                    Some(TokenType::Verb { lemma, .. }) => Some(lemma),
869                    _ => None,
870                }
871            } else {
872                None
873            };
874            let after = if infinitive.is_some() { unit_end + 2 } else { unit_end };
875            if unit_end > self.current
876                && matches!(self.tokens.get(after).map(|t| &t.kind), Some(TokenType::Than))
877            {
878                let mut dim = self.consume_content_word()?;
879                while self.current < unit_end {
880                    let next = self.consume_content_word()?;
881                    dim = self.interner.intern(&format!(
882                        "{}_{}",
883                        self.interner.resolve(dim),
884                        self.interner.resolve(next)
885                    ));
886                }
887                if let Some(vlemma) = infinitive {
888                    self.advance(); // "to"
889                    self.advance(); // the infinitive verb
890                    dim = self.interner.intern(&format!(
891                        "{}_{}",
892                        self.interner.resolve(dim),
893                        self.interner.resolve(vlemma)
894                    ));
895                }
896                if unit_sym.is_none() {
897                    unit_sym = Some(dim);
898                }
899            }
900        }
901
902        // 5b. A per-unit RATE between the comparative and "than" ("less per gallon
903        // than", "5 dollars less per pound than", "10 dollars less per month than")
904        // — the comparison is on a RATE, not a raw amount. The basis is folded into
905        // the measure name (Charge → Charge_per_Gallon) so the prover relates
906        // per-gallon prices; the count's noun ("dollars") becomes the offset unit.
907        let mut rate_unit: Option<Symbol> = None;
908        if matches!(self.peek().kind, TokenType::Preposition(s)
909            if self.interner.resolve(s).eq_ignore_ascii_case("per"))
910        {
911            let cp_rate = self.checkpoint();
912            self.advance(); // "per"
913            if is_unit(&self.peek().kind) {
914                rate_unit = Some(self.consume_content_word()?);
915            } else {
916                self.restore(cp_rate);
917            }
918        }
919
920        // 6. "than" must follow. A measure prefix (count or degree) makes this an
921        // exact/vague arithmetic comparative. A BARE comparative ("cost less than
922        // the potatoes") is also a genuine comparison — handled here for a
923        // solver-ready `Less`/`Greater` over a DISTINCT standard entity — EXCEPT
924        // when the standard is a number ("ate more than 5 apples" is a quantity,
925        // not an entity comparison), which is left for the quantity path.
926        if !matches!(self.peek().kind, TokenType::Than) {
927            self.restore(cp);
928            return Ok(None);
929        }
930        if count_kind.is_none() && !has_vague {
931            // A QUANTIFIED standard ("more than 5 apples" = quantity; "faster than
932            // all cats" = a universally-quantified comparison) is not a bare
933            // entity comparison — leave it for the quantity / verbal-comparative
934            // quantifier paths.
935            let standard_quantified = matches!(
936                self.tokens.get(self.current + 1).map(|t| &t.kind),
937                Some(TokenType::Number(_))
938                    | Some(TokenType::Cardinal(_))
939                    | Some(TokenType::All)
940                    | Some(TokenType::No)
941                    | Some(TokenType::Some)
942                    | Some(TokenType::Any)
943                    | Some(TokenType::Most)
944                    | Some(TokenType::Few)
945                    | Some(TokenType::Many)
946                    | Some(TokenType::AtLeast(_))
947                    | Some(TokenType::AtMost(_))
948            );
949            if standard_quantified {
950                self.restore(cp);
951                return Ok(None);
952            }
953        }
954        self.advance(); // "than"
955
956        let event_var = self.get_event_var();
957        let mut modifiers = Vec::new();
958        let effective_time = self.pending_time.take().unwrap_or(verb_time);
959        match effective_time {
960            Time::Past => modifiers.push(self.interner.intern("Past")),
961            Time::Future => modifiers.push(self.interner.intern("Future")),
962            _ => {}
963        }
964        let suppress_existential = self.drs.in_conditional_antecedent();
965        let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
966            event_var,
967            verb,
968            roles: self
969                .ctx
970                .roles
971                .alloc_slice(vec![(ThematicRole::Agent, subject_term)]),
972            modifiers: self.ctx.syms.alloc_slice(modifiers),
973            suppress_existential,
974            world: None,
975        })));
976
977        // Solver-ready arithmetic: a measure function over the entity, an
978        // arithmetic offset (add/sub — the names the LIA oracle recognises), and
979        // an equality (exact) or strict inequality (vague). The measure is named
980        // by the unit ("points" → Points) or, lacking one, by the verb ("scored"
981        // → Score) — stable across both sides so the prover can relate them.
982        let cap = |s: &str| -> String {
983            let mut c = s.chars();
984            match c.next() {
985                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
986                None => String::new(),
987            }
988        };
989        let measure_name = match (rate_unit, unit_sym) {
990            // A rate names the measure by the VERB per the rate unit; the count's
991            // noun ("dollars") is the offset's unit, not the measure name.
992            (Some(r), _) => format!(
993                "{}_per_{}",
994                cap(&self.interner.resolve(verb).to_string()),
995                cap(&self.interner.resolve(r).to_string())
996            ),
997            (None, Some(u)) => cap(&self.interner.resolve(u).to_string()),
998            (None, None) => cap(&self.interner.resolve(verb).to_string()),
999        };
1000        let measure_sym = self.interner.intern(&measure_name);
1001        // `comp_adj` is the base adjective lemma (e.g. "narrow", "short"); its scale
1002        // polarity decides the direction. Negative-pole adjectives subtract.
1003        let comp_str = self.interner.resolve(comp_adj).to_lowercase();
1004        let subtract = crate::lexicon::is_decreasing_adjective(&comp_str);
1005        let op_sym = self.interner.intern(if subtract { "sub" } else { "add" });
1006        let dir_sym = self.interner.intern(if subtract { "Less" } else { "Greater" });
1007        // For a rate ("5 dollars less per gallon"), the count's noun is the
1008        // offset's currency unit; otherwise the count is implicitly in the measure.
1009        let offset_unit = if rate_unit.is_some() { unit_sym } else { None };
1010        let offset_term: Option<Term<'a>> = count_kind.map(|kind| Term::Value {
1011            kind,
1012            unit: offset_unit,
1013            dimension: None,
1014        });
1015        let measure_x = Term::Function(measure_sym, self.ctx.terms.alloc_slice([subject_term]));
1016        let build_constraint = move |p: &mut Self, y_term: Term<'a>| -> &'a LogicExpr<'a> {
1017            let measure_y = Term::Function(measure_sym, p.ctx.terms.alloc_slice([y_term]));
1018            match offset_term {
1019                Some(off) => {
1020                    let rhs =
1021                        Term::Function(op_sym, p.ctx.terms.alloc_slice([measure_y, off]));
1022                    p.ctx.exprs.alloc(LogicExpr::Identity {
1023                        left: p.ctx.terms.alloc(measure_x),
1024                        right: p.ctx.terms.alloc(rhs),
1025                    })
1026                }
1027                None => p.ctx.exprs.alloc(LogicExpr::Predicate {
1028                    name: dir_sym,
1029                    args: p.ctx.terms.alloc_slice([measure_x, measure_y]),
1030                    world: None,
1031                }),
1032            }
1033        };
1034
1035        // The standard of comparison. A DESCRIPTION (determiner / adjective /
1036        // possessor / PP / relative clause) becomes its own existentially
1037        // quantified entity carrying a restrictor — so "less than Quinn Quade's
1038        // stamp" does NOT collapse onto the subject's "Stamp" constant (which
1039        // would make `Less(Sell(Stamp), Sell(Stamp))` — the subject compared to
1040        // itself — and misattach the possessor to the subject). A bare proper
1041        // name stays a referring constant. Greedy parse so a PP standard ("than
1042        // the perfume from Spain") attaches its PP. A comparative standard is a
1043        // nominal position — "than" rules out the matrix verb — so a verb-word
1044        // head in it is a deverbal noun ("than the orange PACK", "the investing
1045        // SHOW").
1046        let saved_ctx = self.nominal_np_context;
1047        self.nominal_np_context = true;
1048        let std_np_result = self.parse_noun_phrase(true);
1049        self.nominal_np_context = saved_ctx;
1050        let std_np = std_np_result?;
1051        let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
1052        let is_desc = has_rel
1053            || std_np.definiteness.is_some()
1054            || !std_np.adjectives.is_empty()
1055            || std_np.possessor.is_some()
1056            || !std_np.pps.is_empty();
1057        let result = if is_desc {
1058            let std_var = self.next_var_name();
1059            // Head noun + adjectives + possessor over the standard's variable.
1060            let mut restr = self.nominal_predication(Term::Variable(std_var), &std_np);
1061            for pp in std_np.pps {
1062                let pp_sub = self.substitute_pp_placeholder(pp, std_var);
1063                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1064                    left: restr,
1065                    op: TokenType::And,
1066                    right: pp_sub,
1067                });
1068            }
1069            if has_rel {
1070                self.advance(); // "who" / "that"
1071                let rel = self.parse_relative_clause(std_var)?;
1072                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1073                    left: restr,
1074                    op: TokenType::And,
1075                    right: rel,
1076                });
1077            }
1078            let comparison = build_constraint(self, Term::Variable(std_var));
1079            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1080                left: restr,
1081                op: TokenType::And,
1082                right: comparison,
1083            });
1084            let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
1085                kind: QuantifierKind::Existential,
1086                variable: std_var,
1087                body,
1088                island_id: self.current_island,
1089            });
1090            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1091                left: event,
1092                op: TokenType::And,
1093                right: quantified,
1094            })
1095        } else {
1096            let comparison = build_constraint(self, Term::Constant(std_np.noun));
1097            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1098                left: event,
1099                op: TokenType::And,
1100                right: comparison,
1101            })
1102        };
1103        Ok(Some(result))
1104    }
1105
1106    /// Temporal offset after a just-consumed verb: `COUNT CALENDAR-UNIT
1107    /// (after|before) STANDARD` ("performed 2 weeks after Bessie", "will launch
1108    /// 2 months before the graduate who will be studying radiation"). The verb
1109    /// event is asserted and a directed offset relates the subject to the
1110    /// standard. Returns `None` without consuming when the pattern is absent.
1111    /// Bare temporal ordering "(sometime) before/after STANDARD" relating
1112    /// `subject_term` DIRECTLY to a distinct standard entity by time — used by the
1113    /// PASSIVE path ("the photo was taken sometime before the photo of the red
1114    /// panda", "was bought sometime before Faye's pet"), which has no NeoEvent var.
1115    /// Returns `Before/After(subject_term, std)` with the standard a distinct
1116    /// ∃-entity (descriptive) or a constant (bare name). `None` (no consumption) if
1117    /// the pattern is absent. A leading vague adverb ("sometime") is skipped.
1118    pub(super) fn parse_bare_temporal_constraint(
1119        &mut self,
1120        subject_term: Term<'a>,
1121    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
1122        let j = self.current;
1123        let std_at = |k: Option<&TokenType>| {
1124            matches!(
1125                k,
1126                Some(TokenType::Article(_))
1127                    | Some(TokenType::Noun(_))
1128                    | Some(TokenType::ProperName(_))
1129            )
1130        };
1131        let lead_adverb = match self.tokens.get(j).map(|t| &t.kind) {
1132            Some(TokenType::Adverb(_)) => true,
1133            Some(_) => matches!(
1134                self.interner.resolve(self.tokens[j].lexeme).to_lowercase().as_str(),
1135                "sometime" | "shortly" | "soon" | "immediately" | "long" | "right" | "just"
1136            ),
1137            None => false,
1138        };
1139        let dj = if lead_adverb { j + 1 } else { j };
1140        let next_kind = self.tokens.get(dj + 1).map(|t| &t.kind);
1141        // A YEAR or clock time is also a valid temporal reference ("won a prize BEFORE
1142        // 1989", "started AFTER 2010") — the prover orders the value against other
1143        // years/times. Accept a numeric/time-literal object alongside the NP object.
1144        let temporal_at = |k: Option<&TokenType>| {
1145            std_at(k)
1146                || matches!(
1147                    k,
1148                    Some(TokenType::Number(_)) | Some(TokenType::TimeLiteral { .. })
1149                )
1150        };
1151        let bare_dir = match self.tokens.get(dj).map(|t| &t.kind) {
1152            Some(TokenType::Before) if temporal_at(next_kind) => Some("Before"),
1153            Some(TokenType::Preposition(s))
1154                if self.interner.resolve(*s).eq_ignore_ascii_case("before")
1155                    && temporal_at(next_kind) =>
1156            {
1157                Some("Before")
1158            }
1159            Some(TokenType::Preposition(s))
1160                if self.interner.resolve(*s).eq_ignore_ascii_case("after")
1161                    && temporal_at(next_kind) =>
1162            {
1163                Some("After")
1164            }
1165            _ => None,
1166        };
1167        let dir = match bare_dir {
1168            Some(d) => d,
1169            None => return Ok(None),
1170        };
1171        if lead_adverb {
1172            self.advance();
1173        }
1174        self.advance(); // "before" / "after"
1175        let rel_sym = self.interner.intern(dir);
1176        // Numeric temporal reference ("before 1989", "after 2010", "before 9:30am"):
1177        // the year/clock-time is a value, so relate the subject to it directly.
1178        if matches!(
1179            self.peek().kind,
1180            TokenType::Number(_) | TokenType::TimeLiteral { .. }
1181        ) {
1182            let year = self.parse_measure_phrase()?;
1183            return Ok(Some(self.ctx.exprs.alloc(LogicExpr::Predicate {
1184                name: rel_sym,
1185                args: self.ctx.terms.alloc_slice([subject_term, *year]),
1186                world: None,
1187            })));
1188        }
1189        let std_np = self.parse_noun_phrase(true)?;
1190        let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
1191        let is_desc = has_rel
1192            || std_np.definiteness.is_some()
1193            || !std_np.adjectives.is_empty()
1194            || std_np.possessor.is_some()
1195            || !std_np.pps.is_empty();
1196        let result = if is_desc {
1197            let v = self.next_var_name();
1198            let mut restr = self.nominal_predication(Term::Variable(v), &std_np);
1199            for pp in std_np.pps {
1200                let pp_sub = self.substitute_pp_placeholder(pp, v);
1201                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1202                    left: restr,
1203                    op: TokenType::And,
1204                    right: pp_sub,
1205                });
1206            }
1207            if has_rel {
1208                self.advance();
1209                let rel = self.parse_relative_clause(v)?;
1210                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1211                    left: restr,
1212                    op: TokenType::And,
1213                    right: rel,
1214                });
1215            }
1216            let rel = self.ctx.exprs.alloc(LogicExpr::Predicate {
1217                name: rel_sym,
1218                args: self.ctx.terms.alloc_slice([subject_term, Term::Variable(v)]),
1219                world: None,
1220            });
1221            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1222                left: restr,
1223                op: TokenType::And,
1224                right: rel,
1225            });
1226            self.ctx.exprs.alloc(LogicExpr::Quantifier {
1227                kind: QuantifierKind::Existential,
1228                variable: v,
1229                body,
1230                island_id: self.current_island,
1231            })
1232        } else {
1233            self.ctx.exprs.alloc(LogicExpr::Predicate {
1234                name: rel_sym,
1235                args: self.ctx.terms.alloc_slice([subject_term, Term::Constant(std_np.noun)]),
1236                world: None,
1237            })
1238        };
1239        Ok(Some(result))
1240    }
1241
1242    pub(super) fn try_temporal_offset(
1243        &mut self,
1244        verb: Symbol,
1245        subject_term: Term<'a>,
1246        verb_time: Time,
1247    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
1248        let j = self.current;
1249
1250        // Bare temporal ordering (no count/unit): "<verb> before STANDARD" or
1251        // "<verb> after THE/A STANDARD" — relate the subject's event to a
1252        // DISTINCT standard entity. "after <proper name>" is intentionally left
1253        // to the PP-adjunct path (which already yields After(e, Name)); only the
1254        // gaps ("before …", "after the/a …") are filled here.
1255        let std_at = |k: Option<&TokenType>| {
1256            matches!(
1257                k,
1258                Some(TokenType::Article(_))
1259                    | Some(TokenType::Noun(_))
1260                    | Some(TokenType::ProperName(_))
1261            )
1262        };
1263        // A vague temporal adverb may precede the direction ("starts SOMETIME
1264        // after …", "arrived SHORTLY before …"); it only emphasises the absence
1265        // of a precise offset, which the bare relation already captures, so skip
1266        // it. `dj` indexes the after/before once it is skipped.
1267        let lead_adverb = match self.tokens.get(j).map(|t| &t.kind) {
1268            Some(TokenType::Adverb(_)) => true,
1269            Some(_) => matches!(
1270                self.interner.resolve(self.tokens[j].lexeme).to_lowercase().as_str(),
1271                "sometime" | "shortly" | "soon" | "immediately" | "long" | "right" | "just"
1272            ),
1273            None => false,
1274        };
1275        let dj = if lead_adverb { j + 1 } else { j };
1276        let next_kind = self.tokens.get(dj + 1).map(|t| &t.kind);
1277        // A YEAR or clock time is a temporal reference the prover can order ("happened
1278        // BEFORE 1989", "started AFTER 2010"). "after <name>" still defers to the
1279        // PP-adjunct path, but "after <year>" has no such path, so accept it here.
1280        let num_at = |k: Option<&TokenType>| {
1281            matches!(
1282                k,
1283                Some(TokenType::Number(_)) | Some(TokenType::TimeLiteral { .. })
1284            )
1285        };
1286        let bare_dir = match self.tokens.get(dj).map(|t| &t.kind) {
1287            Some(TokenType::Before) if std_at(next_kind) || num_at(next_kind) => Some("Before"),
1288            Some(TokenType::Preposition(s))
1289                if self.interner.resolve(*s).eq_ignore_ascii_case("before")
1290                    && (std_at(next_kind) || num_at(next_kind)) =>
1291            {
1292                Some("Before")
1293            }
1294            Some(TokenType::Preposition(s))
1295                if self.interner.resolve(*s).eq_ignore_ascii_case("after")
1296                    && (matches!(next_kind, Some(TokenType::Article(_))) || num_at(next_kind)) =>
1297            {
1298                Some("After")
1299            }
1300            _ => None,
1301        };
1302        if let Some(dir) = bare_dir {
1303            if lead_adverb {
1304                self.advance(); // skip the vague temporal adverb
1305            }
1306            self.advance(); // "before" / "after"
1307            let event_var = self.get_event_var();
1308            let mut modifiers = Vec::new();
1309            let effective_time = self.pending_time.take().unwrap_or(verb_time);
1310            match effective_time {
1311                Time::Past => modifiers.push(self.interner.intern("Past")),
1312                Time::Future => modifiers.push(self.interner.intern("Future")),
1313                _ => {}
1314            }
1315            let suppress_existential = self.drs.in_conditional_antecedent();
1316            let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1317                event_var,
1318                verb,
1319                roles: self
1320                    .ctx
1321                    .roles
1322                    .alloc_slice(vec![(ThematicRole::Agent, subject_term)]),
1323                modifiers: self.ctx.syms.alloc_slice(modifiers),
1324                suppress_existential,
1325                world: None,
1326            })));
1327            let rel_sym = self.interner.intern(dir);
1328            // Numeric temporal reference ("happened before 1989", "started after
1329            // 2010"): relate the EVENT to the year/clock-time value directly.
1330            if matches!(
1331                self.peek().kind,
1332                TokenType::Number(_) | TokenType::TimeLiteral { .. }
1333            ) {
1334                let year = self.parse_measure_phrase()?;
1335                let rel = self.ctx.exprs.alloc(LogicExpr::Predicate {
1336                    name: rel_sym,
1337                    args: self
1338                        .ctx
1339                        .terms
1340                        .alloc_slice([Term::Variable(event_var), *year]),
1341                    world: None,
1342                });
1343                return Ok(Some(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1344                    left: event,
1345                    op: TokenType::And,
1346                    right: rel,
1347                })));
1348            }
1349            // A temporal standard ("after the skydiving TRIP") is a nominal
1350            // position — a verb-word head there is a deverbal noun.
1351            let saved_ctx = self.nominal_np_context;
1352            self.nominal_np_context = true;
1353            let std_np_result = self.parse_noun_phrase(true);
1354            self.nominal_np_context = saved_ctx;
1355            let std_np = std_np_result?;
1356            let is_desc = std_np.definiteness.is_some()
1357                || !std_np.adjectives.is_empty()
1358                || std_np.possessor.is_some()
1359                || !std_np.pps.is_empty();
1360            let result = if is_desc {
1361                let v = self.next_var_name();
1362                let mut restr = self.nominal_predication(Term::Variable(v), &std_np);
1363                for pp in std_np.pps {
1364                    let pp_sub = self.substitute_pp_placeholder(pp, v);
1365                    restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1366                        left: restr,
1367                        op: TokenType::And,
1368                        right: pp_sub,
1369                    });
1370                }
1371                // A relative clause on the standard ("before the winner WHO won in
1372                // chemistry", "after the person WHO took the cruise") restricts the
1373                // standard entity; without this it was stranded (TrailingTokens at
1374                // Who/That). Mirrors parse_bare_temporal_constraint and the offset path.
1375                if self.check(&TokenType::Who) || self.check(&TokenType::That) {
1376                    self.advance();
1377                    let rc = self.parse_relative_clause(v)?;
1378                    restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1379                        left: restr,
1380                        op: TokenType::And,
1381                        right: rc,
1382                    });
1383                }
1384                let rel = self.ctx.exprs.alloc(LogicExpr::Predicate {
1385                    name: rel_sym,
1386                    args: self
1387                        .ctx
1388                        .terms
1389                        .alloc_slice([Term::Variable(event_var), Term::Variable(v)]),
1390                    world: None,
1391                });
1392                let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1393                    left: restr,
1394                    op: TokenType::And,
1395                    right: rel,
1396                });
1397                let quant = self.ctx.exprs.alloc(LogicExpr::Quantifier {
1398                    kind: QuantifierKind::Existential,
1399                    variable: v,
1400                    body,
1401                    island_id: self.current_island,
1402                });
1403                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1404                    left: event,
1405                    op: TokenType::And,
1406                    right: quant,
1407                })
1408            } else {
1409                let rel = self.ctx.exprs.alloc(LogicExpr::Predicate {
1410                    name: rel_sym,
1411                    args: self
1412                        .ctx
1413                        .terms
1414                        .alloc_slice([Term::Variable(event_var), Term::Constant(std_np.noun)]),
1415                    world: None,
1416                });
1417                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1418                    left: event,
1419                    op: TokenType::And,
1420                    right: rel,
1421                })
1422            };
1423            return Ok(Some(result));
1424        }
1425
1426        // "N <unit> after/before STANDARD" — a solver-ready measure offset. The
1427        // event is asserted and a constraint relates the two positions. The
1428        // constraint builder is shared with the PASSIVE path ("was taken 1 month
1429        // after Y"), which already has its own event, so it is factored out into
1430        // `parse_temporal_offset_constraint`.
1431        let has_count = matches!(
1432            self.tokens.get(j).map(|t| &t.kind),
1433            Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
1434        );
1435        let has_unit = matches!(
1436            self.tokens.get(j + 1).map(|t| &t.kind),
1437            Some(TokenType::CalendarUnit(_))
1438        );
1439        let has_dir = matches!(self.tokens.get(j + 2).map(|t| &t.kind), Some(TokenType::Before))
1440            || matches!(self.tokens.get(j + 2).map(|t| &t.kind),
1441                Some(TokenType::Preposition(s))
1442                    if matches!(self.interner.resolve(*s).to_lowercase().as_str(), "after" | "before"));
1443        if !(has_count && has_unit && has_dir) {
1444            return Ok(None);
1445        }
1446
1447        // Capture tense BEFORE parsing the standard, whose relative clause could
1448        // otherwise overwrite pending_time.
1449        let effective_time = self.pending_time.take().unwrap_or(verb_time);
1450        let constraint = match self.parse_temporal_offset_constraint(subject_term)? {
1451            Some(c) => c,
1452            None => return Ok(None),
1453        };
1454        let event_var = self.get_event_var();
1455        let mut modifiers = Vec::new();
1456        match effective_time {
1457            Time::Past => modifiers.push(self.interner.intern("Past")),
1458            Time::Future => modifiers.push(self.interner.intern("Future")),
1459            _ => {}
1460        }
1461        let suppress_existential = self.drs.in_conditional_antecedent();
1462        let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1463            event_var,
1464            verb,
1465            roles: self
1466                .ctx
1467                .roles
1468                .alloc_slice(vec![(ThematicRole::Agent, subject_term)]),
1469            modifiers: self.ctx.syms.alloc_slice(modifiers),
1470            suppress_existential,
1471            world: None,
1472        })));
1473        Ok(Some(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1474            left: event,
1475            op: TokenType::And,
1476            right: constraint,
1477        })))
1478    }
1479
1480    /// Parses a calendar-unit measure offset `N <unit> (after|before) STANDARD`
1481    /// positioned at the count, returning ONLY the solver-ready constraint
1482    /// `Unit(subject) = add|sub(Unit(STANDARD), N)` (after → add, before → sub).
1483    /// The caller supplies the event (active VP) or passive predicate it conjoins
1484    /// to. A descriptive standard becomes a distinct existential entity carrying
1485    /// its restrictor (head + adjectives + possessor + PPs + relative clause); a
1486    /// bare proper name stays a constant. `None` (no consumption) if the offset
1487    /// pattern is absent.
1488    pub(super) fn parse_temporal_offset_constraint(
1489        &mut self,
1490        subject_term: Term<'a>,
1491    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
1492        let j = self.current;
1493        let has_count = matches!(
1494            self.tokens.get(j).map(|t| &t.kind),
1495            Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
1496        );
1497        let has_unit = matches!(
1498            self.tokens.get(j + 1).map(|t| &t.kind),
1499            Some(TokenType::CalendarUnit(_))
1500        );
1501        let direction = match self.tokens.get(j + 2).map(|t| &t.kind) {
1502            Some(TokenType::Before) => Some("Before"),
1503            Some(TokenType::Preposition(s)) => {
1504                match self.interner.resolve(*s).to_lowercase().as_str() {
1505                    "after" => Some("After"),
1506                    "before" => Some("Before"),
1507                    _ => None,
1508                }
1509            }
1510            _ => None,
1511        };
1512        if !(has_count && has_unit && direction.is_some()) {
1513            return Ok(None);
1514        }
1515        let direction = direction.unwrap();
1516
1517        // Offset count (Number or Cardinal) and calendar unit.
1518        let count_kind = match self.advance().kind {
1519            TokenType::Number(s) => {
1520                let raw = self.interner.resolve(s);
1521                if let Ok(n) = raw.parse::<i64>() {
1522                    crate::ast::NumberKind::Integer(n)
1523                } else {
1524                    crate::ast::NumberKind::Symbolic(s)
1525                }
1526            }
1527            TokenType::Cardinal(n) => crate::ast::NumberKind::Integer(n as i64),
1528            _ => unreachable!("guarded by has_count"),
1529        };
1530        let unit_lexeme = self.peek().lexeme;
1531        self.advance(); // calendar unit
1532        self.advance(); // "after" / "before"
1533
1534        let cap = |s: &str| -> String {
1535            let mut c = s.chars();
1536            match c.next() {
1537                Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
1538                None => String::new(),
1539            }
1540        };
1541        let measure_sym = self.interner.intern(&cap(&self.interner.resolve(unit_lexeme).to_string()));
1542        let op_sym = self.interner.intern(if direction == "After" { "add" } else { "sub" });
1543        let offset_term = Term::Value {
1544            kind: count_kind,
1545            unit: None,
1546            dimension: None,
1547        };
1548
1549        let measure_x = Term::Function(measure_sym, self.ctx.terms.alloc_slice([subject_term]));
1550        let build_constraint = move |p: &mut Self, y_term: Term<'a>| -> &'a LogicExpr<'a> {
1551            let measure_y = Term::Function(measure_sym, p.ctx.terms.alloc_slice([y_term]));
1552            let rhs = Term::Function(op_sym, p.ctx.terms.alloc_slice([measure_y, offset_term]));
1553            p.ctx.exprs.alloc(LogicExpr::Identity {
1554                left: p.ctx.terms.alloc(measure_x),
1555                right: p.ctx.terms.alloc(rhs),
1556            })
1557        };
1558
1559        // Distinct-identity treatment: a descriptive standard ("2 weeks after
1560        // Quinn Quade's debut") becomes its own existential entity so it never
1561        // collapses onto the subject and its possessor/PP/relative clause bind to
1562        // IT; a bare name stays a constant. The standard is nominal ("after"
1563        // rules out the matrix verb) → a verb-word head is a deverbal noun
1564        // ("1 month after the goblin shark PROJECT").
1565        let saved_ctx = self.nominal_np_context;
1566        self.nominal_np_context = true;
1567        let std_np_result = self.parse_noun_phrase(true);
1568        self.nominal_np_context = saved_ctx;
1569        let std_np = std_np_result?;
1570        let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
1571        let is_desc = has_rel
1572            || std_np.definiteness.is_some()
1573            || !std_np.adjectives.is_empty()
1574            || std_np.possessor.is_some()
1575            || !std_np.pps.is_empty();
1576        let result = if is_desc {
1577            let std_var = self.next_var_name();
1578            let mut restr = self.nominal_predication(Term::Variable(std_var), &std_np);
1579            for pp in std_np.pps {
1580                let pp_sub = self.substitute_pp_placeholder(pp, std_var);
1581                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1582                    left: restr,
1583                    op: TokenType::And,
1584                    right: pp_sub,
1585                });
1586            }
1587            if has_rel {
1588                self.advance(); // "who" / "that"
1589                let rel = self.parse_relative_clause(std_var)?;
1590                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1591                    left: restr,
1592                    op: TokenType::And,
1593                    right: rel,
1594                });
1595            }
1596            let relation = build_constraint(self, Term::Variable(std_var));
1597            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1598                left: restr,
1599                op: TokenType::And,
1600                right: relation,
1601            });
1602            self.ctx.exprs.alloc(LogicExpr::Quantifier {
1603                kind: QuantifierKind::Existential,
1604                variable: std_var,
1605                body,
1606                island_id: self.current_island,
1607            })
1608        } else {
1609            build_constraint(self, Term::Constant(std_np.noun))
1610        };
1611        Ok(Some(result))
1612    }
1613
1614    /// Ordinal-position offset after a just-consumed verb: `COUNT
1615    /// (place|places|spot|spots) (ahead of | behind | before | after) STANDARD`
1616    /// ("finished 2 places ahead of Bob", "performed 1 spot before Violet"). The
1617    /// verb event is asserted and a directed offset orders the two positions:
1618    /// `Place(X) = add|sub(Place(Y), N)` (ahead/before → sub, behind/after → add),
1619    /// solver-ready, with the standard as a DISTINCT entity. `None` if absent.
1620    pub(super) fn try_positional_offset(
1621        &mut self,
1622        verb: Symbol,
1623        subject_term: Term<'a>,
1624        verb_time: Time,
1625    ) -> ParseResult<Option<&'a LogicExpr<'a>>> {
1626        let j = self.current;
1627        let has_count = matches!(
1628            self.tokens.get(j).map(|t| &t.kind),
1629            Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
1630        );
1631        let is_pos_unit = self
1632            .tokens
1633            .get(j + 1)
1634            .map(|t| {
1635                matches!(
1636                    self.interner.resolve(t.lexeme).to_lowercase().as_str(),
1637                    "place" | "places" | "spot" | "spots"
1638                )
1639            })
1640            .unwrap_or(false);
1641        if !(has_count && is_pos_unit) {
1642            return Ok(None);
1643        }
1644        // Direction word at j+2: ahead(/of) / before → sub; behind / after → add.
1645        let (subtract, ahead_of) = match self.tokens.get(j + 2).map(|t| &t.kind) {
1646            Some(TokenType::Before) => (true, false),
1647            Some(_) => match self
1648                .interner
1649                .resolve(self.tokens[j + 2].lexeme)
1650                .to_lowercase()
1651                .as_str()
1652            {
1653                "ahead" => (true, true),
1654                "before" => (true, false),
1655                "behind" => (false, false),
1656                "after" => (false, false),
1657                _ => return Ok(None),
1658            },
1659            None => return Ok(None),
1660        };
1661
1662        let count_kind = match self.advance().kind {
1663            TokenType::Number(s) => {
1664                let raw = self.interner.resolve(s);
1665                if let Ok(n) = raw.parse::<i64>() {
1666                    crate::ast::NumberKind::Integer(n)
1667                } else {
1668                    crate::ast::NumberKind::Symbolic(s)
1669                }
1670            }
1671            TokenType::Cardinal(n) => crate::ast::NumberKind::Integer(n as i64),
1672            _ => unreachable!("guarded by has_count"),
1673        };
1674        self.advance(); // positional unit
1675        self.advance(); // direction word
1676        if ahead_of
1677            && matches!(self.peek().kind, TokenType::Preposition(s)
1678                if self.interner.resolve(s).eq_ignore_ascii_case("of"))
1679        {
1680            self.advance(); // "of" after "ahead"
1681        }
1682
1683        let measure_sym = self.interner.intern("Place");
1684        let op_sym = self.interner.intern(if subtract { "sub" } else { "add" });
1685        let offset_term = Term::Value { kind: count_kind, unit: None, dimension: None };
1686
1687        let event_var = self.get_event_var();
1688        let mut modifiers = Vec::new();
1689        let effective_time = self.pending_time.take().unwrap_or(verb_time);
1690        match effective_time {
1691            Time::Past => modifiers.push(self.interner.intern("Past")),
1692            Time::Future => modifiers.push(self.interner.intern("Future")),
1693            _ => {}
1694        }
1695        let suppress_existential = self.drs.in_conditional_antecedent();
1696        let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1697            event_var,
1698            verb,
1699            roles: self.ctx.roles.alloc_slice(vec![(ThematicRole::Agent, subject_term)]),
1700            modifiers: self.ctx.syms.alloc_slice(modifiers),
1701            suppress_existential,
1702            world: None,
1703        })));
1704
1705        let measure_x = Term::Function(measure_sym, self.ctx.terms.alloc_slice([subject_term]));
1706        let build_constraint = move |p: &mut Self, y_term: Term<'a>| -> &'a LogicExpr<'a> {
1707            let measure_y = Term::Function(measure_sym, p.ctx.terms.alloc_slice([y_term]));
1708            let rhs = Term::Function(op_sym, p.ctx.terms.alloc_slice([measure_y, offset_term]));
1709            p.ctx.exprs.alloc(LogicExpr::Identity {
1710                left: p.ctx.terms.alloc(measure_x),
1711                right: p.ctx.terms.alloc(rhs),
1712            })
1713        };
1714
1715        let std_np = self.parse_noun_phrase(true)?;
1716        let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
1717        let is_desc = has_rel
1718            || std_np.definiteness.is_some()
1719            || !std_np.adjectives.is_empty()
1720            || std_np.possessor.is_some()
1721            || !std_np.pps.is_empty();
1722        let result = if is_desc {
1723            let std_var = self.next_var_name();
1724            let mut restr = self.nominal_predication(Term::Variable(std_var), &std_np);
1725            for pp in std_np.pps {
1726                let pp_sub = self.substitute_pp_placeholder(pp, std_var);
1727                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1728                    left: restr,
1729                    op: TokenType::And,
1730                    right: pp_sub,
1731                });
1732            }
1733            if has_rel {
1734                self.advance();
1735                let rel = self.parse_relative_clause(std_var)?;
1736                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1737                    left: restr,
1738                    op: TokenType::And,
1739                    right: rel,
1740                });
1741            }
1742            let relation = build_constraint(self, Term::Variable(std_var));
1743            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1744                left: restr,
1745                op: TokenType::And,
1746                right: relation,
1747            });
1748            let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
1749                kind: QuantifierKind::Existential,
1750                variable: std_var,
1751                body,
1752                island_id: self.current_island,
1753            });
1754            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1755                left: event,
1756                op: TokenType::And,
1757                right: quantified,
1758            })
1759        } else {
1760            let relation = build_constraint(self, Term::Constant(std_np.noun));
1761            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
1762                left: event,
1763                op: TokenType::And,
1764                right: relation,
1765            })
1766        };
1767        Ok(Some(result))
1768    }
1769
1770    fn parse_predicate_impl(
1771        &mut self,
1772        subject_symbol: Symbol,
1773        as_variable: bool,
1774    ) -> ParseResult<&'a LogicExpr<'a>> {
1775        let subject_term = if as_variable {
1776            Term::Variable(subject_symbol)
1777        } else {
1778            Term::Constant(subject_symbol)
1779        };
1780
1781        // Weather verb + expletive "it" detection: "it rains" → ∃e(Rain(e))
1782        let subject_str = self.interner.resolve(subject_symbol).to_lowercase();
1783        if subject_str == "it" && self.check_verb() {
1784            if let TokenType::Verb { lemma, time, .. } = &self.peek().kind {
1785                let lemma_str = self.interner.resolve(*lemma);
1786                if Lexer::is_weather_verb(lemma_str) {
1787                    let verb = *lemma;
1788                    let verb_time = *time;
1789                    self.advance(); // consume the weather verb
1790
1791                    let event_var = self.get_event_var();
1792                    let suppress_existential = self.drs.in_conditional_antecedent();
1793                    if suppress_existential {
1794                        let event_class = self.interner.intern("Event");
1795                        self.drs.introduce_referent(event_var, event_class, Gender::Neuter, Number::Singular);
1796                    }
1797                    let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1798                        event_var,
1799                        verb,
1800                        roles: self.ctx.roles.alloc_slice(vec![]), // No thematic roles
1801                        modifiers: self.ctx.syms.alloc_slice(vec![]),
1802                        suppress_existential,
1803                        world: None,
1804                    })));
1805
1806                    return Ok(match verb_time {
1807                        Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
1808                            operator: TemporalOperator::Past,
1809                            body: neo_event,
1810                        }),
1811                        Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
1812                            operator: TemporalOperator::Future,
1813                            body: neo_event,
1814                        }),
1815                        _ => neo_event,
1816                    });
1817                }
1818            }
1819        }
1820
1821        // Weather adjective + expletive "it" detection: "it is wet" → Wet
1822        // Also handle "it's wet" where 's is Possessive token
1823        if subject_str == "it" && (self.check(&TokenType::Is) || self.check(&TokenType::Was) || self.check(&TokenType::Possessive)) {
1824            let saved_pos = self.current;
1825            self.advance(); // consume copula
1826
1827            if self.check_content_word() {
1828                let adj_lexeme = self.peek().lexeme;
1829                let adj_str = self.interner.resolve(adj_lexeme).to_lowercase();
1830
1831                if let Some(meta) = crate::lexicon::lookup_adjective_db(&adj_str) {
1832                    if meta.features.contains(&crate::lexicon::Feature::Weather) {
1833                        let adj_sym = self.consume_content_word().unwrap_or(adj_lexeme);
1834                        // Atmospheric predicate: "it is wet" → Wet
1835                        return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
1836                            name: adj_sym,
1837                            args: self.ctx.terms.alloc_slice([]),
1838                            world: None,
1839                        }));
1840                    }
1841                }
1842            }
1843            // Not a weather adjective, restore position
1844            self.current = saved_pos;
1845        }
1846
1847        if self.check(&TokenType::Never) {
1848            self.advance();
1849            let verb = self.consume_verb();
1850            let verb_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1851                name: verb,
1852                args: self.ctx.terms.alloc_slice([subject_term]),
1853                world: None,
1854            });
1855            return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1856                op: TokenType::Not,
1857                operand: verb_pred,
1858            }));
1859        }
1860
1861        if self.check_modal() {
1862            return self.parse_aspect_chain_with_term(subject_term.clone());
1863        }
1864
1865        if self.check_content_word() {
1866            let next_word = self.interner.resolve(self.peek().lexeme).to_lowercase();
1867            if next_word == "has" || next_word == "have" || next_word == "had" {
1868                // Look ahead to distinguish perfect aspect ("has eaten") from possession ("has 3 children")
1869                // Perfect aspect: has/have/had + verb
1870                // Possession: has/have/had + number/noun
1871                let is_perfect_aspect = if self.current + 1 < self.tokens.len() {
1872                    let next_token = &self.tokens[self.current + 1].kind;
1873                    matches!(
1874                        next_token,
1875                        TokenType::Verb { .. } | TokenType::Not
1876                    ) && !matches!(next_token, TokenType::Number(_))
1877                } else {
1878                    false
1879                };
1880                if is_perfect_aspect {
1881                    return self.parse_aspect_chain(subject_symbol);
1882                }
1883                // Otherwise, treat "has" as a main verb (possession) and continue below
1884            }
1885        }
1886
1887        if self.check(&TokenType::Had) {
1888            return self.parse_aspect_chain(subject_symbol);
1889        }
1890
1891        // Handle do-support: "I do/don't know who"
1892        if self.check(&TokenType::Does) || self.check(&TokenType::Do) {
1893            self.advance();
1894            let is_negated = self.match_token(&[TokenType::Not]);
1895
1896            if self.check(&TokenType::Ever) {
1897                self.advance();
1898            }
1899
1900            if self.check_verb() {
1901                let (verb, verb_time, verb_aspect, verb_class) =
1902                    self.consume_verb_with_metadata();
1903
1904                // Check for embedded wh-clause with sluicing: "I don't know who"
1905                if self.check_wh_word() {
1906                    let wh_token = self.advance().kind.clone();
1907                    let is_who = matches!(wh_token, TokenType::Who);
1908                    let is_what = matches!(wh_token, TokenType::What);
1909
1910                    let is_sluicing = self.is_at_end() ||
1911                        self.check(&TokenType::Period) ||
1912                        self.check(&TokenType::Comma);
1913
1914                    if is_sluicing {
1915                        if let Some(template) = self.last_event_template.clone() {
1916                            let wh_var = self.next_var_name();
1917
1918                            let roles: Vec<_> = if is_who {
1919                                std::iter::once((ThematicRole::Agent, Term::Variable(wh_var)))
1920                                    .chain(template.non_agent_roles.iter().cloned())
1921                                    .collect()
1922                            } else if is_what {
1923                                vec![
1924                                    (ThematicRole::Agent, subject_term.clone()),
1925                                    (ThematicRole::Theme, Term::Variable(wh_var)),
1926                                ]
1927                            } else {
1928                                std::iter::once((ThematicRole::Agent, Term::Variable(wh_var)))
1929                                    .chain(template.non_agent_roles.iter().cloned())
1930                                    .collect()
1931                            };
1932
1933                            let event_var = self.get_event_var();
1934                            let suppress_existential = self.drs.in_conditional_antecedent();
1935                            let reconstructed = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1936                                event_var,
1937                                verb: template.verb,
1938                                roles: self.ctx.roles.alloc_slice(roles),
1939                                modifiers: self.ctx.syms.alloc_slice(template.modifiers.clone()),
1940                                suppress_existential,
1941                                world: None,
1942                            })));
1943
1944                            let question = self.ctx.exprs.alloc(LogicExpr::Question {
1945                                wh_variable: wh_var,
1946                                body: reconstructed,
1947                            });
1948
1949                            let know_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
1950                                event_var: self.get_event_var(),
1951                                verb,
1952                                roles: self.ctx.roles.alloc_slice(vec![
1953                                    (ThematicRole::Agent, subject_term.clone()),
1954                                    (ThematicRole::Theme, Term::Proposition(question)),
1955                                ]),
1956                                modifiers: self.ctx.syms.alloc_slice(vec![]),
1957                                suppress_existential,
1958                                world: None,
1959                            })));
1960
1961                            let result = if is_negated {
1962                                self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1963                                    op: TokenType::Not,
1964                                    operand: know_event,
1965                                })
1966                            } else {
1967                                know_event
1968                            };
1969
1970                            return Ok(result);
1971                        }
1972                    }
1973                }
1974
1975                // Regular do-support ("does/do/don't VERB …"): delegate the whole
1976                // VP — object, measure phrase, PPs, aspect — to the shared builder
1977                // so every complement form folds exactly as in the positive path,
1978                // wrapping in ¬ when "not"/"never" was present. The negative scope
1979                // is open across the complement so NPIs inside it are licensed.
1980                if is_negated {
1981                    self.negative_depth += 1;
1982                }
1983                let vp = self.build_verb_vp(
1984                    subject_symbol,
1985                    subject_term,
1986                    as_variable,
1987                    verb,
1988                    verb_time,
1989                    verb_aspect,
1990                    verb_class,
1991                )?;
1992                if is_negated {
1993                    self.negative_depth -= 1;
1994                    return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
1995                        op: TokenType::Not,
1996                        operand: vp,
1997                    }));
1998                }
1999                return Ok(vp);
2000            }
2001        }
2002
2003        // Check for auxiliary (like "did" in "did not bark")
2004        // BUT: "did it" should be parsed as verb "do" with object "it"
2005        // We lookahead to check if this is truly an auxiliary usage
2006        if self.check_auxiliary() && self.is_true_auxiliary_usage() {
2007            let aux_time = if let TokenType::Auxiliary(time) = self.advance().kind {
2008                time
2009            } else {
2010                Time::None
2011            };
2012            self.pending_time = Some(aux_time);
2013
2014            if self.match_token(&[TokenType::Not]) {
2015                self.negative_depth += 1;
2016
2017                // A bare verb the lexicon ALSO lists as a performative ("didn't ORDER",
2018                // "didn't CALL") is the clause's main verb after do-support, not a
2019                // speech act — re-tag it so check_verb consumes it (the tense comes from
2020                // the "did"/"does" auxiliary).
2021                if let TokenType::Performative(_) = self.peek().kind {
2022                    let lemma = self
2023                        .interner
2024                        .intern(&self.interner.resolve(self.peek().lexeme).to_lowercase());
2025                    self.tokens[self.current].kind = TokenType::Verb {
2026                        lemma,
2027                        time: Time::None,
2028                        aspect: Aspect::Simple,
2029                        class: crate::lexicon::VerbClass::Activity,
2030                    };
2031                }
2032                // Check for verb or "do" (TokenType::Do is separate from TokenType::Verb)
2033                if self.check_verb() || self.check(&TokenType::Do) {
2034                    let (verb, verb_time, verb_aspect, verb_class) =
2035                        if self.check(&TokenType::Do) {
2036                            self.advance(); // consume "do"
2037                            (
2038                                self.interner.intern("Do"),
2039                                Time::None,
2040                                Aspect::Simple,
2041                                crate::lexicon::VerbClass::Activity,
2042                            )
2043                        } else {
2044                            self.consume_verb_with_metadata()
2045                        };
2046
2047                    if self.check_quantifier() {
2048                        let quantifier_token = self.advance().kind.clone();
2049                        let object_np = self.parse_noun_phrase(false)?;
2050                        let obj_var = self.next_var_name();
2051
2052                        let obj_restriction = self.ctx.exprs.alloc(LogicExpr::Predicate {
2053                            name: object_np.noun,
2054                            args: self.ctx.terms.alloc_slice([Term::Variable(obj_var)]),
2055                            world: None,
2056                        });
2057
2058                        let verb_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2059                            name: verb,
2060                            args: self
2061                                .ctx
2062                                .terms
2063                                .alloc_slice([subject_term, Term::Variable(obj_var)]),
2064                            world: None,
2065                        });
2066
2067                        let (kind, body) = match quantifier_token {
2068                            TokenType::Any => {
2069                                if self.is_negative_context() {
2070                                    (
2071                                        QuantifierKind::Existential,
2072                                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2073                                            left: obj_restriction,
2074                                            op: TokenType::And,
2075                                            right: verb_pred,
2076                                        }),
2077                                    )
2078                                } else {
2079                                    (
2080                                        QuantifierKind::Universal,
2081                                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2082                                            left: obj_restriction,
2083                                            op: TokenType::Implies,
2084                                            right: verb_pred,
2085                                        }),
2086                                    )
2087                                }
2088                            }
2089                            TokenType::Some => (
2090                                QuantifierKind::Existential,
2091                                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2092                                    left: obj_restriction,
2093                                    op: TokenType::And,
2094                                    right: verb_pred,
2095                                }),
2096                            ),
2097                            TokenType::All => (
2098                                QuantifierKind::Universal,
2099                                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2100                                    left: obj_restriction,
2101                                    op: TokenType::Implies,
2102                                    right: verb_pred,
2103                                }),
2104                            ),
2105                            _ => (
2106                                QuantifierKind::Existential,
2107                                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2108                                    left: obj_restriction,
2109                                    op: TokenType::And,
2110                                    right: verb_pred,
2111                                }),
2112                            ),
2113                        };
2114
2115                        let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
2116                            kind,
2117                            variable: obj_var,
2118                            body,
2119                            island_id: self.current_island,
2120                        });
2121
2122                        let effective_time = self.pending_time.take().unwrap_or(Time::None);
2123                        let with_time = match effective_time {
2124                            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
2125                                operator: TemporalOperator::Past,
2126                                body: quantified,
2127                            }),
2128                            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
2129                                operator: TemporalOperator::Future,
2130                                body: quantified,
2131                            }),
2132                            _ => quantified,
2133                        };
2134
2135                        self.negative_depth -= 1;
2136                        return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2137                            op: TokenType::Not,
2138                            operand: with_time,
2139                        }));
2140                    }
2141
2142                    if self.check_npi_object() {
2143                        let npi_token = self.advance().kind.clone();
2144                        let obj_var = self.next_var_name();
2145
2146                        let restriction_name = match npi_token {
2147                            TokenType::Anything => "Thing",
2148                            TokenType::Anyone => "Person",
2149                            _ => "Thing",
2150                        };
2151
2152                        let restriction_sym = self.interner.intern(restriction_name);
2153                        let obj_restriction = self.ctx.exprs.alloc(LogicExpr::Predicate {
2154                            name: restriction_sym,
2155                            args: self.ctx.terms.alloc_slice([Term::Variable(obj_var)]),
2156                            world: None,
2157                        });
2158
2159                        let verb_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2160                            name: verb,
2161                            args: self.ctx.terms.alloc_slice([subject_term, Term::Variable(obj_var)]),
2162                            world: None,
2163                        });
2164
2165                        let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2166                            left: obj_restriction,
2167                            op: TokenType::And,
2168                            right: verb_pred,
2169                        });
2170
2171                        let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
2172                            kind: QuantifierKind::Existential,
2173                            variable: obj_var,
2174                            body,
2175                            island_id: self.current_island,
2176                        });
2177
2178                        let effective_time = self.pending_time.take().unwrap_or(Time::None);
2179                        let with_time = match effective_time {
2180                            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
2181                                operator: TemporalOperator::Past,
2182                                body: quantified,
2183                            }),
2184                            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
2185                                operator: TemporalOperator::Future,
2186                                body: quantified,
2187                            }),
2188                            _ => quantified,
2189                        };
2190
2191                        self.negative_depth -= 1;
2192                        return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2193                            op: TokenType::Not,
2194                            operand: with_time,
2195                        }));
2196                    }
2197
2198                    // Delegate the VP complement (object/measure/PP/aspect) to the
2199                    // shared builder so negated do-support folds every complement
2200                    // form exactly as the positive path does. pending_time still
2201                    // carries the auxiliary's tense ("did" → Past) into the build.
2202                    let vp = self.build_verb_vp(
2203                        subject_symbol,
2204                        subject_term,
2205                        as_variable,
2206                        verb,
2207                        verb_time,
2208                        verb_aspect,
2209                        verb_class,
2210                    )?;
2211
2212                    self.negative_depth -= 1;
2213                    return Ok(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2214                        op: TokenType::Not,
2215                        operand: vp,
2216                    }));
2217                }
2218
2219                self.negative_depth -= 1;
2220            }
2221        }
2222
2223        if self.check(&TokenType::Is)
2224            || self.check(&TokenType::Are)
2225            || self.check(&TokenType::Was)
2226            || self.check(&TokenType::Were)
2227        {
2228            let copula_time = if self.check(&TokenType::Was) || self.check(&TokenType::Were) {
2229                Time::Past
2230            } else {
2231                Time::Present
2232            };
2233            self.advance();
2234
2235            // Check for negation: "was not caught", "is not happy"
2236            let is_negated = self.check(&TokenType::Not);
2237            if is_negated {
2238                self.advance(); // consume "not"
2239            }
2240
2241            // Check for temporal adverbs after copula: "is eventually Y", "is always Y", "is never Y"
2242            let mut copula_temporal: Option<super::CopulaTemporal> = None;
2243            if !is_negated {
2244                if self.check(&TokenType::Never) {
2245                    self.advance();
2246                    copula_temporal = Some(super::CopulaTemporal::Never);
2247                } else if let TokenType::Adverb(sym) | TokenType::ScopalAdverb(sym) | TokenType::TemporalAdverb(sym) = &self.peek().kind {
2248                    let resolved = self.interner.resolve(*sym).to_string();
2249                    if resolved == "Always" || resolved == "always" {
2250                        self.advance();
2251                        copula_temporal = Some(super::CopulaTemporal::Always);
2252                    } else if resolved == "Eventually" || resolved == "eventually" {
2253                        self.advance();
2254                        copula_temporal = Some(super::CopulaTemporal::Eventually);
2255                    }
2256                }
2257            }
2258
2259            if self.check_verb() {
2260                let (verb, _verb_time, verb_aspect, verb_class) = self.consume_verb_with_metadata();
2261
2262                // Stative verbs cannot be progressive
2263                if verb_class.is_stative() && verb_aspect == Aspect::Progressive {
2264                    return Err(crate::error::ParseError {
2265                        kind: crate::error::ParseErrorKind::StativeProgressiveConflict,
2266                        span: self.current_span(),
2267                    });
2268                }
2269
2270                let mut predicate: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::Predicate {
2271                    name: verb,
2272                    args: self.ctx.terms.alloc_slice([subject_term]),
2273                    world: None,
2274                });
2275
2276                // Passive by-phrase: the NP after `by` is the AGENT and must fill
2277                // the predicate's agent (FIRST) slot — `See(Mary, John)` for
2278                // "John was seen by Mary" — matching the main passive path. Handle
2279                // it BEFORE the generic locative-PP loop below, which would
2280                // otherwise demote the agent into a spurious `by(theme, agent)`.
2281                if self.check_by_preposition() {
2282                    self.advance(); // consume "by"
2283                    if self.check_content_word()
2284                        || matches!(self.peek().kind, TokenType::Article(_))
2285                    {
2286                        let agent = self.parse_noun_phrase(true)?;
2287                        // A DESCRIPTIVE by-agent becomes its own restrictor-carrying
2288                        // entity scoping the relation; a bare one keeps the constant.
2289                        let (agent_term, agent_restr) = self.possessor_entity(&agent);
2290                        let core = self.ctx.exprs.alloc(LogicExpr::Predicate {
2291                            name: verb,
2292                            args: self.ctx.terms.alloc_slice([agent_term, subject_term]),
2293                            world: None,
2294                        });
2295                        predicate = self.wrap_in_possessor_entity(agent_restr, core);
2296                    }
2297                }
2298
2299                // Trailing PP adjuncts on the passive participle ("was found in
2300                // Spain", "was taken on May 12", "was at 88.2 W") and a calendar-unit
2301                // offset ("was taken 1 month after Y") — predicated of the theme.
2302                // This makes the embedded VP parser (of-pair members, delegated
2303                // relative clauses) as capable as parse_atom's main passive path.
2304                while self.check_preposition() && !self.check_of_preposition()
2305                    && !self.pp_is_cycle_temporal()
2306                {
2307                    let prep = match self.advance().kind {
2308                        TokenType::Preposition(s) => s,
2309                        _ => break,
2310                    };
2311                    let adjunct = if self.check_number() {
2312                        let m = self.parse_measure_phrase()?;
2313                        self.ctx.exprs.alloc(LogicExpr::Predicate {
2314                            name: prep,
2315                            args: self.ctx.terms.alloc_slice([subject_term, *m]),
2316                            world: None,
2317                        })
2318                    } else if self.check_content_word()
2319                        || matches!(self.peek().kind, TokenType::Article(_))
2320                    {
2321                        let obj = self.parse_noun_phrase(true)?;
2322                        self.ctx.exprs.alloc(LogicExpr::Predicate {
2323                            name: prep,
2324                            args: self.ctx.terms.alloc_slice([subject_term, Term::Constant(obj.noun)]),
2325                            world: None,
2326                        })
2327                    } else {
2328                        break;
2329                    };
2330                    predicate = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2331                        left: predicate,
2332                        op: TokenType::And,
2333                        right: adjunct,
2334                    });
2335                }
2336                if let Some(constraint) = self.parse_temporal_offset_constraint(subject_term)? {
2337                    predicate = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2338                        left: predicate,
2339                        op: TokenType::And,
2340                        right: constraint,
2341                    });
2342                }
2343
2344                let with_aspect = if verb_aspect == Aspect::Progressive {
2345                    // Semelfactive + Progressive → Iterative
2346                    let operator = if verb_class == crate::lexicon::VerbClass::Semelfactive {
2347                        AspectOperator::Iterative
2348                    } else {
2349                        AspectOperator::Progressive
2350                    };
2351                    self.ctx.exprs.alloc(LogicExpr::Aspectual {
2352                        operator,
2353                        body: predicate,
2354                    })
2355                } else {
2356                    predicate
2357                };
2358
2359                let with_time = if copula_time == Time::Past {
2360                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2361                        operator: TemporalOperator::Past,
2362                        body: with_aspect,
2363                    })
2364                } else {
2365                    with_aspect
2366                };
2367
2368                let with_neg = if is_negated {
2369                    self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2370                        op: TokenType::Not,
2371                        operand: with_time,
2372                    })
2373                } else {
2374                    with_time
2375                };
2376
2377                let result = match copula_temporal {
2378                    Some(super::CopulaTemporal::Always) => {
2379                        self.ctx.exprs.alloc(LogicExpr::Temporal {
2380                            operator: TemporalOperator::Always,
2381                            body: with_neg,
2382                        })
2383                    }
2384                    Some(super::CopulaTemporal::Never) => {
2385                        let negated = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2386                            op: TokenType::Not,
2387                            operand: with_time,
2388                        });
2389                        self.ctx.exprs.alloc(LogicExpr::Temporal {
2390                            operator: TemporalOperator::Always,
2391                            body: negated,
2392                        })
2393                    }
2394                    Some(super::CopulaTemporal::Eventually) => {
2395                        self.ctx.exprs.alloc(LogicExpr::Temporal {
2396                            operator: TemporalOperator::Eventually,
2397                            body: with_neg,
2398                        })
2399                    }
2400                    None => with_neg,
2401                };
2402
2403                return Ok(result);
2404            }
2405
2406            // "is 14 inches tall" / "is 5 meters long" — a measure phrase + a
2407            // dimensional adjective: Adj(subject, measure). Also bare "is 98.6
2408            // degrees" → Identity. parse_atom handles this for its subjects; the
2409            // of-pair / quantified-subject copula VPs ("the other is 14 inches
2410            // tall") reach here. A measure-OFFSET comparative ("is 2 inches
2411            // taller than X") is left to fall through (not mis-read as Identity).
2412            if self.check_number() {
2413                let after_measure_is_comparative = {
2414                    let mut i = self.current + 1; // past the number
2415                    if matches!(
2416                        self.tokens.get(i).map(|t| &t.kind),
2417                        Some(TokenType::Noun(_)) | Some(TokenType::CalendarUnit(_))
2418                    ) {
2419                        i += 1; // past an optional unit word
2420                    }
2421                    matches!(
2422                        self.tokens.get(i).map(|t| &t.kind),
2423                        Some(TokenType::Comparative(_))
2424                    )
2425                };
2426                if !after_measure_is_comparative {
2427                    let measure = self.parse_measure_phrase()?;
2428                    let pred = if self.check_content_word() {
2429                        let adj = self.consume_content_word()?;
2430                        self.ctx.exprs.alloc(LogicExpr::Predicate {
2431                            name: adj,
2432                            args: self.ctx.terms.alloc_slice([subject_term, *measure]),
2433                            world: None,
2434                        })
2435                    } else {
2436                        self.ctx.exprs.alloc(LogicExpr::Identity {
2437                            left: self.ctx.terms.alloc(subject_term),
2438                            right: measure,
2439                        })
2440                    };
2441                    return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2442                }
2443            }
2444
2445            // "is on Rosewood Street" / "is from Australia" — locative/origin PP in copula position.
2446            // Excludes "by" which is handled as passive-agent above.
2447            if self.check_preposition() && !self.check_by_preposition() {
2448                let prep_token = self.advance().clone();
2449                let prep_sym = match prep_token.kind {
2450                    TokenType::Preposition(s) => s,
2451                    _ => unreachable!("guarded by check_preposition()"),
2452                };
2453                // A coordinate / measure object ("is at 88.2 W", "is at 40.5 N")
2454                // routes through parse_measure_phrase, which takes the trailing
2455                // direction/unit; otherwise a plain NP object.
2456                let base = if self.check_number() {
2457                    let m = self.parse_measure_phrase()?;
2458                    self.ctx.exprs.alloc(LogicExpr::Predicate {
2459                        name: prep_sym,
2460                        args: self.ctx.terms.alloc_slice([subject_term, *m]),
2461                        world: None,
2462                    })
2463                } else {
2464                    let pp_obj = self.parse_noun_phrase(true)?;
2465                    self.ctx.exprs.alloc(LogicExpr::Predicate {
2466                        name: prep_sym,
2467                        args: self.ctx.terms.alloc_slice([subject_term, Term::Constant(pp_obj.noun)]),
2468                        world: None,
2469                    })
2470                };
2471                let with_time = if copula_time == Time::Past {
2472                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2473                        operator: TemporalOperator::Past,
2474                        body: base,
2475                    })
2476                } else {
2477                    base
2478                };
2479                return Ok(if is_negated {
2480                    self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2481                        op: TokenType::Not,
2482                        operand: with_time,
2483                    })
2484                } else {
2485                    with_time
2486                });
2487            }
2488
2489            // "is Tara" — identity with a proper name (X = Tara), enabling Leibniz's
2490            // Law. But "is Kerry's project" is a POSSESSIVE NP complement
2491            // (Project(x) ∧ Possesses(Kerry, x)), so a following "'s" diverts to NP
2492            // predication instead of the bare identity.
2493            if let TokenType::ProperName(pname) = self.peek().kind {
2494                if matches!(
2495                    self.tokens.get(self.current + 1).map(|t| &t.kind),
2496                    Some(TokenType::Possessive)
2497                ) {
2498                    // "is Ginger's." — an ELIDED possessed noun (a clause boundary
2499                    // follows "'s") → Possesses(Ginger, subject). parse_atom's
2500                    // copula handles this; the of-pair / quantified-subject VPs
2501                    // route here, where it previously failed (ExpectedContentWord
2502                    // at the period). "is Ginger's PROJECT" still parses the full
2503                    // possessive NP below.
2504                    let elided = matches!(
2505                        self.tokens.get(self.current + 2).map(|t| &t.kind),
2506                        Some(TokenType::Period) | Some(TokenType::EOF)
2507                            | Some(TokenType::Comma) | Some(TokenType::And)
2508                            | Some(TokenType::Or) | None
2509                    );
2510                    if elided {
2511                        self.advance(); // proper name
2512                        self.advance(); // possessive
2513                        let pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2514                            name: self.interner.intern("Possesses"),
2515                            args: self.ctx.terms.alloc_slice([
2516                                Term::Constant(pname),
2517                                subject_term,
2518                            ]),
2519                            world: None,
2520                        });
2521                        return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2522                    }
2523                    let saved_ctx = self.nominal_np_context;
2524                    self.nominal_np_context = true;
2525                    let np_result = self.parse_noun_phrase(true);
2526                    self.nominal_np_context = saved_ctx;
2527                    let np = np_result?;
2528                    let pred = self.nominal_predication_with_pps(subject_term, &np);
2529                    return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2530                }
2531
2532                // A possessive after a MULTI-WORD proper name ("is Tim Tucker's
2533                // film") — the single-word check above misses it (the second name
2534                // sits where the "'s" would be). Absorb the full possessor, then
2535                // predicate the possessed noun: Possesses(Tim_Tucker, x) ∧ Film(x).
2536                let multiword_poss = {
2537                    let mut k = self.current + 1;
2538                    while matches!(self.tokens.get(k).map(|t| &t.kind), Some(TokenType::ProperName(_))) {
2539                        k += 1;
2540                    }
2541                    if k > self.current + 1
2542                        && matches!(self.tokens.get(k).map(|t| &t.kind), Some(TokenType::Possessive))
2543                    {
2544                        Some(k)
2545                    } else {
2546                        None
2547                    }
2548                };
2549                if let Some(poss_pos) = multiword_poss {
2550                    let elided = matches!(
2551                        self.tokens.get(poss_pos + 1).map(|t| &t.kind),
2552                        Some(TokenType::Period) | Some(TokenType::EOF)
2553                            | Some(TokenType::Comma) | Some(TokenType::And)
2554                            | Some(TokenType::Or) | None
2555                    );
2556                    self.advance(); // first proper name
2557                    let possessor = self.absorb_multiword_proper_name(pname);
2558                    self.advance(); // possessive
2559                    let poss_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2560                        name: self.interner.intern("Possesses"),
2561                        args: self.ctx.terms.alloc_slice([Term::Constant(possessor), subject_term]),
2562                        world: None,
2563                    });
2564                    let pred = if elided {
2565                        poss_pred
2566                    } else {
2567                        let saved_ctx = self.nominal_np_context;
2568                        self.nominal_np_context = true;
2569                        let np_result = self.parse_noun_phrase(true);
2570                        self.nominal_np_context = saved_ctx;
2571                        let np = np_result?;
2572                        let np_pred = self.nominal_predication_with_pps(subject_term, &np);
2573                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2574                            left: np_pred,
2575                            op: TokenType::And,
2576                            right: poss_pred,
2577                        })
2578                    };
2579                    return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2580                }
2581
2582                self.advance();
2583                // Absorb subsequent capitalized words into one multi-word proper
2584                // name ("Porcher Place", "Highland Drive") — a place/title name is
2585                // a single entity, so the identity must not strand the second word.
2586                let pname = self.absorb_multiword_proper_name(pname);
2587                let identity = self.ctx.exprs.alloc(LogicExpr::Identity {
2588                    left: self.ctx.terms.alloc(subject_term),
2589                    right: self.ctx.terms.alloc(Term::Constant(pname)),
2590                });
2591                return Ok(self.finish_copula(identity, copula_time, is_negated, copula_temporal));
2592            }
2593
2594            // "is either A or B" — disjunctive predication: A(x) ∨ B(x).
2595            if self.check(&TokenType::Either) {
2596                self.advance(); // consume "either"
2597                let saved_ctx = self.nominal_np_context;
2598                self.nominal_np_context = true;
2599                let np1_result = self.parse_noun_phrase(true);
2600                self.nominal_np_context = saved_ctx;
2601                let np1 = np1_result?;
2602                // Keep the disjunct's PP restrictors ("either the vegetables FROM
2603                // JESUP or …") — bare nominal_predication drops them, the same
2604                // meaning-loss the plain copula complement avoids with _with_pps.
2605                let pred1 = self.nominal_predication_with_pps(subject_term, &np1);
2606                // A relative clause on the disjunct ("either the one WHO won or …")
2607                // attaches before "or"; the second disjunct's after np2.
2608                let pred1 = self.conjoin_trailing_relative(pred1, subject_term)?;
2609                if self.check(&TokenType::Or) {
2610                    self.advance(); // consume "or"
2611                    let saved_ctx2 = self.nominal_np_context;
2612                    self.nominal_np_context = true;
2613                    let np2_result = self.parse_noun_phrase(true);
2614                    self.nominal_np_context = saved_ctx2;
2615                    let np2 = np2_result?;
2616                    let pred2 = self.nominal_predication_with_pps(subject_term, &np2);
2617                    let pred2 = self.conjoin_trailing_relative(pred2, subject_term)?;
2618                    let disj = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2619                        left: pred1,
2620                        op: TokenType::Or,
2621                        right: pred2,
2622                    });
2623                    return Ok(self.finish_copula(disj, copula_time, is_negated, copula_temporal));
2624                }
2625                return Ok(self.finish_copula(pred1, copula_time, is_negated, copula_temporal));
2626            }
2627
2628            // "is the mansion" / "is the frat on Holly Street" / "is the Alvarado
2629            // family's house" — NP predication keeping the genitive AND the
2630            // predicate NP's PP restrictors ("on Holly Street"); dropping the PP
2631            // is a meaning-loss parse.
2632            if self.check_article() {
2633                let saved_ctx = self.nominal_np_context;
2634                self.nominal_np_context = true;
2635                let pred_np_result = self.parse_noun_phrase(true);
2636                self.nominal_np_context = saved_ctx;
2637                let pred_np = pred_np_result?;
2638                let pred = self.nominal_predication_with_pps(subject_term, &pred_np);
2639                // A relative clause on the predicate nominal ("was the player WHO
2640                // played", "is the one THAT won") is predicated of the subject —
2641                // being that player entails the subject played. neither/nor and
2642                // quantified subjects route their copula complement through here.
2643                let pred = self.conjoin_trailing_relative(pred, subject_term)?;
2644                return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2645            }
2646
2647            // "is older" / "is faster than the cobra" — a comparative copula
2648            // complement. parse_atom routes its subjects to parse_comparative;
2649            // quantified / of-pair subjects (parse_predicate_with_subject) reach
2650            // here, so the comparative must be handled too. Use the COMPARATIVE
2651            // surface ("older" → Older), not the base lemma, so the degree
2652            // survives. Bare (no "than") → unary property ("one is older"); with
2653            // "than" → binary comparison.
2654            if let TokenType::Comparative(_) = self.peek().kind {
2655                let comp_tok = self.advance().clone();
2656                let comp_surface = self.interner.resolve(comp_tok.lexeme).to_string();
2657                let comp_name = {
2658                    let mut c = comp_surface.chars();
2659                    match c.next() {
2660                        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
2661                        None => comp_surface.clone(),
2662                    }
2663                };
2664                let name = self.interner.intern(&comp_name);
2665                let pred = if self.check(&TokenType::Than) {
2666                    self.advance(); // than
2667                    let std_np = self.parse_noun_phrase(true)?;
2668                    let std = self.nominal_predication(Term::Constant(std_np.noun), &std_np);
2669                    let cmp = self.ctx.exprs.alloc(LogicExpr::Predicate {
2670                        name,
2671                        args: self.ctx.terms.alloc_slice([
2672                            subject_term,
2673                            Term::Constant(std_np.noun),
2674                        ]),
2675                        world: None,
2676                    });
2677                    // Keep the standard's own restrictors (a bare proper name's
2678                    // std is vacuous Predicate, harmless).
2679                    if matches!(std, LogicExpr::Predicate { args, .. } if args.len() == 1) {
2680                        cmp
2681                    } else {
2682                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2683                            left: cmp,
2684                            op: TokenType::And,
2685                            right: std,
2686                        })
2687                    }
2688                } else {
2689                    self.ctx.exprs.alloc(LogicExpr::Predicate {
2690                        name,
2691                        args: self.ctx.terms.alloc_slice([subject_term]),
2692                        world: None,
2693                    })
2694                };
2695                return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2696            }
2697
2698            // Copula complement led by a temporal/ordinal adverb: "was FIRST", "is
2699            // NOW the leader". Shared with parse_atom's copula path.
2700            if let Some(base) = self.copula_temporal_adverb_complement(subject_term.clone())? {
2701                return Ok(self.finish_copula(base, copula_time, is_negated, copula_temporal));
2702            }
2703
2704            let predicate = self.consume_content_word()?;
2705
2706            // Coordinated predicate adjectives — "is black and red" (or "black &
2707            // red", where "&" lexes to "and") → Adj1(subj) ∧ Adj2(subj). Mirrors
2708            // parse_atom; of-pair / neither / quantified subjects reach this shared
2709            // VP parser. Requires "and" before each extra adjective.
2710            {
2711                let mut coord_adjs: Vec<Symbol> = vec![predicate];
2712                while self.check(&TokenType::And) {
2713                    let saved = self.current;
2714                    self.advance();
2715                    if let TokenType::Adjective(a) = self.peek().kind {
2716                        // "and ADJ is/are/verb …" — ADJ is the SUBJECT of a new
2717                        // clause, not a coordinated predicate adjective.
2718                        if matches!(
2719                            self.tokens.get(self.current + 1).map(|t| &t.kind),
2720                            Some(TokenType::Is) | Some(TokenType::Are)
2721                                | Some(TokenType::Was) | Some(TokenType::Were)
2722                                | Some(TokenType::Verb { .. })
2723                        ) {
2724                            self.current = saved;
2725                            break;
2726                        }
2727                        self.advance();
2728                        coord_adjs.push(a);
2729                    } else {
2730                        self.current = saved;
2731                        break;
2732                    }
2733                }
2734                if coord_adjs.len() > 1 {
2735                    let mut conj: &'a LogicExpr<'a> = self.ctx.exprs.alloc(LogicExpr::Predicate {
2736                        name: coord_adjs[0],
2737                        args: self.ctx.terms.alloc_slice([subject_term.clone()]),
2738                        world: None,
2739                    });
2740                    for &a in &coord_adjs[1..] {
2741                        let p = self.ctx.exprs.alloc(LogicExpr::Predicate {
2742                            name: a,
2743                            args: self.ctx.terms.alloc_slice([subject_term.clone()]),
2744                            world: None,
2745                        });
2746                        conj = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
2747                            left: conj,
2748                            op: TokenType::And,
2749                            right: p,
2750                        });
2751                    }
2752                    return Ok(self.finish_copula(conj, copula_time, is_negated, copula_temporal));
2753                }
2754            }
2755
2756            // Postposed measure complement on a predicate adjective — "is worth
2757            // $26 billion", "is worth 5 dollars" → Worth(subject, $26 billion).
2758            // Mirrors parse_atom's copula path; of-pair / neither / quantified
2759            // subjects reach this shared VP parser, so the complement lives here
2760            // too. A number after a predicate adjective is never a separate clause.
2761            if self.check_number() {
2762                let measure = self.parse_measure_phrase()?;
2763                let pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2764                    name: predicate,
2765                    args: self.ctx.terms.alloc_slice([subject_term.clone(), *measure]),
2766                    world: None,
2767                });
2768                return Ok(self.finish_copula(pred, copula_time, is_negated, copula_temporal));
2769            }
2770
2771            let base_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
2772                name: predicate,
2773                args: self.ctx.terms.alloc_slice([subject_term]),
2774                world: None,
2775            });
2776
2777            let with_time = if copula_time == Time::Past {
2778                self.ctx.exprs.alloc(LogicExpr::Temporal {
2779                    operator: TemporalOperator::Past,
2780                    body: base_pred,
2781                })
2782            } else {
2783                base_pred
2784            };
2785
2786            let with_neg = if is_negated {
2787                self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2788                    op: TokenType::Not,
2789                    operand: with_time,
2790                })
2791            } else {
2792                with_time
2793            };
2794
2795            let result = match copula_temporal {
2796                Some(super::CopulaTemporal::Always) => {
2797                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2798                        operator: TemporalOperator::Always,
2799                        body: with_neg,
2800                    })
2801                }
2802                Some(super::CopulaTemporal::Never) => {
2803                    let negated = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
2804                        op: TokenType::Not,
2805                        operand: with_time,
2806                    });
2807                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2808                        operator: TemporalOperator::Always,
2809                        body: negated,
2810                    })
2811                }
2812                Some(super::CopulaTemporal::Eventually) => {
2813                    self.ctx.exprs.alloc(LogicExpr::Temporal {
2814                        operator: TemporalOperator::Eventually,
2815                        body: with_neg,
2816                    })
2817                }
2818                None => with_neg,
2819            };
2820
2821            return Ok(result);
2822        }
2823
2824        // Handle "did it" - when Auxiliary(Past) is used as a transitive verb (past of "do")
2825        // This happens when we bypassed auxiliary handling because of lookahead
2826        if self.check_auxiliary_as_main_verb() {
2827            return self.parse_do_as_main_verb(subject_term);
2828        }
2829
2830        self.parse_finite_verb_vp(subject_symbol, subject_term, as_variable)
2831    }
2832
2833    /// Parses a finite verb's VP body: the verb, its object/measure/coordinated/
2834    /// ditransitive complement, trailing PPs, object-internal adjectives/PPs, and
2835    /// aspectual operators — building the NeoEvent. Shared by the positive predicate
2836    /// path and the do-support paths ("does/do/did not VERB …"), which wrap the
2837    /// result in negation; unifying them so every complement form folds uniformly.
2838    pub(super) fn parse_finite_verb_vp(
2839        &mut self,
2840        subject_symbol: Symbol,
2841        subject_term: Term<'a>,
2842        as_variable: bool,
2843    ) -> ParseResult<&'a LogicExpr<'a>> {
2844        if self.check_verb() {
2845            let (verb, verb_time, verb_aspect, verb_class) = self.consume_verb_with_metadata();
2846            self.build_verb_vp(
2847                subject_symbol,
2848                subject_term,
2849                as_variable,
2850                verb,
2851                verb_time,
2852                verb_aspect,
2853                verb_class,
2854            )
2855        } else {
2856            Ok(self.ctx.exprs.alloc(LogicExpr::Atom(subject_symbol)))
2857        }
2858    }
2859
2860    /// The VP body shared by the positive predicate path and the do-support paths
2861    /// ("does/do/did not VERB …"): the object/measure/coordinated/ditransitive
2862    /// complement, trailing PPs, object-internal adjectives/PPs, and aspectual
2863    /// operators — building and returning the NeoEvent. The caller consumes the
2864    /// verb (with metadata); negated do-support wraps the result in ¬.
2865    ///
2866    /// Coordinated OBJECT LISTS ("Determine each trip's activity, state and year, as
2867    /// well as the friend …") are handled here: the first object's predication is
2868    /// built by [`build_verb_vp_single`], then each comma/and-separated additional
2869    /// object yields its own predication of the SAME verb, all conjoined. A leading
2870    /// determiner/possessor ("each trip's") distributes over the bare coordinate
2871    /// heads (it is replayed before each), while a coordinate carrying its own
2872    /// determiner ("the friend …") is parsed as a fresh NP. No member is dropped.
2873    pub(super) fn build_verb_vp(
2874        &mut self,
2875        subject_symbol: Symbol,
2876        subject_term: Term<'a>,
2877        as_variable: bool,
2878        verb: Symbol,
2879        verb_time: Time,
2880        verb_aspect: Aspect,
2881        verb_class: crate::lexicon::VerbClass,
2882    ) -> ParseResult<&'a LogicExpr<'a>> {
2883        // The leading determiner/possessor that distributes over a coordinate head
2884        // list ("each trip's" in "each trip's activity, state and year"): a
2885        // quantifier optionally followed by a possessive NP, captured as raw tokens
2886        // to replay before each bare coordinate. Empty when the object opens with no
2887        // shared determiner.
2888        let shared_prefix = self.capture_distributive_prefix();
2889
2890        let first = self.build_verb_vp_single(
2891            subject_symbol,
2892            subject_term.clone(),
2893            as_variable,
2894            verb,
2895            verb_time,
2896            verb_aspect,
2897            verb_class,
2898        )?;
2899
2900        let mut combined = first;
2901        loop {
2902            // Consume a coordinator: "," / "and" / ", and" / ", as well as" (the MWE
2903            // collapses "as well as" to And, so ", as well as X" arrives as Comma
2904            // And X). A trailing comma with no following object is not a coordinator.
2905            let mut k = self.current;
2906            let mut saw_coordinator = false;
2907            if matches!(self.tokens.get(k).map(|t| &t.kind), Some(TokenType::Comma)) {
2908                k += 1;
2909                saw_coordinator = true;
2910            }
2911            if matches!(self.tokens.get(k).map(|t| &t.kind), Some(TokenType::And)) {
2912                k += 1;
2913                saw_coordinator = true;
2914            }
2915            if !saw_coordinator {
2916                break;
2917            }
2918            // An object must actually follow the coordinator — a determiner, a
2919            // content word, or a number. Otherwise this is sentential/VP "and" (not
2920            // ours to consume), so leave it for the surrounding parser.
2921            let opens_object = matches!(
2922                self.tokens.get(k).map(|t| &t.kind),
2923                Some(TokenType::Article(_))
2924                    | Some(TokenType::All)
2925                    | Some(TokenType::Some)
2926                    | Some(TokenType::No)
2927                    | Some(TokenType::Any)
2928                    | Some(TokenType::Most)
2929                    | Some(TokenType::Few)
2930                    | Some(TokenType::Many)
2931                    | Some(TokenType::Noun(_))
2932                    | Some(TokenType::ProperName(_))
2933                    | Some(TokenType::CalendarUnit(_))
2934                    | Some(TokenType::Ambiguous { .. })
2935                    | Some(TokenType::Number(_))
2936                    | Some(TokenType::Cardinal(_))
2937            );
2938            if !opens_object {
2939                break;
2940            }
2941            // CLAUSE-BOUNDARY GUARD: a determiner/adjective-led NP whose COMMON-NOUN
2942            // head is immediately followed by a finite verb is the SUBJECT of a new
2943            // clause, not a coordinated object — "enters the room, the alarm TRIGGERS"
2944            // must not coordinate "the room, the alarm". A pure-noun head must be
2945            // crossed first, so a bare ambiguous noun/verb coordinate head ("…, STATE
2946            // and year") is still coordinated and a proper-name reduced relative ("the
2947            // friend SIMON went with") is not mistaken for a clause.
2948            {
2949                let mut p = k;
2950                if matches!(
2951                    self.tokens.get(p).map(|t| &t.kind),
2952                    Some(TokenType::Article(_)) | Some(TokenType::All) | Some(TokenType::Some)
2953                        | Some(TokenType::No) | Some(TokenType::Any) | Some(TokenType::Most)
2954                        | Some(TokenType::Few) | Some(TokenType::Many)
2955                ) {
2956                    p += 1;
2957                }
2958                while matches!(
2959                    self.tokens.get(p).map(|t| &t.kind),
2960                    Some(TokenType::Adjective(_)) | Some(TokenType::NonIntersectiveAdjective(_))
2961                ) {
2962                    p += 1;
2963                }
2964                let head_start = p;
2965                while matches!(self.tokens.get(p).map(|t| &t.kind), Some(TokenType::Noun(_))) {
2966                    p += 1;
2967                }
2968                let saw_noun_head = p > head_start;
2969                let verb_follows = self.tokens.get(p).map_or(false, |t| {
2970                    self.kind_is_verb(&t.kind)
2971                        || matches!(
2972                            t.kind,
2973                            TokenType::Auxiliary(_)
2974                                | TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
2975                                | TokenType::Must | TokenType::Can | TokenType::Should
2976                                | TokenType::Could | TokenType::Would | TokenType::May
2977                                | TokenType::Might | TokenType::Shall | TokenType::Cannot
2978                        )
2979                });
2980                if saw_noun_head && verb_follows {
2981                    break;
2982                }
2983            }
2984            // Commit to consuming the coordinator tokens.
2985            self.current = k;
2986            // A bare coordinate head (no determiner of its own) inherits the shared
2987            // distributive prefix; one with its own determiner is parsed as-is.
2988            let has_own_determiner = matches!(
2989                self.tokens.get(self.current).map(|t| &t.kind),
2990                Some(TokenType::Article(_))
2991                    | Some(TokenType::All)
2992                    | Some(TokenType::Some)
2993                    | Some(TokenType::No)
2994                    | Some(TokenType::Any)
2995                    | Some(TokenType::Most)
2996                    | Some(TokenType::Few)
2997                    | Some(TokenType::Many)
2998            );
2999            if !has_own_determiner && !shared_prefix.is_empty() {
3000                self.splice_tokens(self.current, &shared_prefix);
3001            }
3002            let next = self.build_verb_vp_single(
3003                subject_symbol,
3004                subject_term.clone(),
3005                as_variable,
3006                verb,
3007                verb_time,
3008                verb_aspect,
3009                verb_class,
3010            )?;
3011            combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3012                left: combined,
3013                op: TokenType::And,
3014                right: next,
3015            });
3016        }
3017        Ok(combined)
3018    }
3019
3020    /// Capture the leading distributive determiner/possessor of the object at the
3021    /// cursor as cloned tokens to be replayed before bare coordinate heads — a
3022    /// quantifier (`each`/`every`/…) optionally followed by a possessive NP
3023    /// (`trip 's`). Returns an empty vector and leaves the cursor unchanged when the
3024    /// object opens with no such prefix (a definite article, a bare noun, …).
3025    fn capture_distributive_prefix(&self) -> Vec<Token> {
3026        let mut p = self.current;
3027        let mut prefix: Vec<Token> = Vec::new();
3028        let is_quantifier = matches!(
3029            self.tokens.get(p).map(|t| &t.kind),
3030            Some(TokenType::All)
3031                | Some(TokenType::Some)
3032                | Some(TokenType::No)
3033                | Some(TokenType::Any)
3034                | Some(TokenType::Most)
3035                | Some(TokenType::Few)
3036                | Some(TokenType::Many)
3037        );
3038        if !is_quantifier {
3039            return prefix;
3040        }
3041        prefix.push(self.tokens[p].clone());
3042        p += 1;
3043        // An optional possessive NP head ("trip 's"): one or more nouns then "'s".
3044        let mut q = p;
3045        let mut saw_noun = false;
3046        while matches!(
3047            self.tokens.get(q).map(|t| &t.kind),
3048            Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
3049        ) {
3050            saw_noun = true;
3051            q += 1;
3052        }
3053        if saw_noun
3054            && matches!(
3055                self.tokens.get(q).map(|t| &t.kind),
3056                Some(TokenType::Possessive)
3057            )
3058        {
3059            for t in &self.tokens[p..=q] {
3060                prefix.push(t.clone());
3061            }
3062        }
3063        prefix
3064    }
3065
3066    /// Insert `toks` into the token stream at index `at`, shifting the tail right.
3067    fn splice_tokens(&mut self, at: usize, toks: &[Token]) {
3068        for (i, t) in toks.iter().enumerate() {
3069            self.tokens.insert(at + i, t.clone());
3070        }
3071    }
3072
3073    /// Builds the predication for a SINGLE object (see [`build_verb_vp`], which wraps
3074    /// this to coordinate object lists). The caller has consumed the verb.
3075    pub(super) fn build_verb_vp_single(
3076        &mut self,
3077        subject_symbol: Symbol,
3078        subject_term: Term<'a>,
3079        as_variable: bool,
3080        mut verb: Symbol,
3081        verb_time: Time,
3082        verb_aspect: Aspect,
3083        verb_class: crate::lexicon::VerbClass,
3084    ) -> ParseResult<&'a LogicExpr<'a>> {
3085            let mut args = vec![subject_term.clone()];
3086
3087            // Control/raising verb with infinitival complement ("wants to
3088            // play"): route through the canonical control machinery, then
3089            // restore the subject's variable-ness so quantified subjects bind
3090            // into the complement ("Every child wants to play." → W(x, Play(x))).
3091            if self.is_control_verb(verb) && self.check_to() {
3092                let subject_np = NounPhrase {
3093                    noun: subject_symbol,
3094                    definiteness: None,
3095                    adjectives: &[],
3096                    possessor: None,
3097                    pps: &[],
3098                    superlative: None,
3099                };
3100                let control = self.parse_control_structure(&subject_np, verb, verb_time)?;
3101                return if as_variable {
3102                    self.substitute_constant_with_var(control, subject_symbol, subject_symbol)
3103                } else {
3104                    Ok(control)
3105                };
3106            }
3107
3108            // Arithmetic / vague verbal comparative ("scored 3 points lower
3109            // than Bessie", "scored somewhat higher than Shirley"). Shared
3110            // with parse_atom's inline VP path via try_arithmetic_comparative.
3111            if let Some(cmp) = self.try_arithmetic_comparative(verb, subject_term.clone(), verb_time)? {
3112                return Ok(cmp);
3113            }
3114
3115            // Temporal offset ("performed 2 weeks after Bessie").
3116            if let Some(off) = self.try_temporal_offset(verb, subject_term.clone(), verb_time)? {
3117                return Ok(off);
3118            }
3119
3120            // Ordinal-position offset ("finished 2 places ahead of Bob").
3121            if let Some(off) = self.try_positional_offset(verb, subject_term.clone(), verb_time)? {
3122                return Ok(off);
3123            }
3124
3125            // Verbal comparative ("runs faster than Bob", "run faster than all
3126            // cats"): the comparative grades the event participants — the verb
3127            // event is asserted and the subject compared to the standard.
3128            if let TokenType::Comparative(comp_adj) = self.peek().kind.clone() {
3129                if matches!(
3130                    self.tokens.get(self.current + 1).map(|t| t.kind.clone()),
3131                    Some(TokenType::Than)
3132                ) {
3133                    self.advance(); // comparative
3134                    self.advance(); // than
3135
3136                    let event_var = self.get_event_var();
3137                    let mut modifiers = Vec::new();
3138                    let effective_time = self.pending_time.take().unwrap_or(verb_time);
3139                    match effective_time {
3140                        Time::Past => modifiers.push(self.interner.intern("Past")),
3141                        Time::Future => modifiers.push(self.interner.intern("Future")),
3142                        _ => {}
3143                    }
3144                    let suppress_existential = self.drs.in_conditional_antecedent();
3145                    let event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3146                        event_var,
3147                        verb,
3148                        roles: self
3149                            .ctx
3150                            .roles
3151                            .alloc_slice(vec![(ThematicRole::Agent, subject_term.clone())]),
3152                        modifiers: self.ctx.syms.alloc_slice(modifiers),
3153                        suppress_existential,
3154                        world: None,
3155                    })));
3156
3157                    let result = if self.check_quantifier() {
3158                        let q = self.advance().kind.clone();
3159                        let std_np = self.parse_noun_phrase(false)?;
3160                        let std_var = self.next_var_name();
3161                        let restriction = self.ctx.exprs.alloc(LogicExpr::Predicate {
3162                            name: std_np.noun,
3163                            args: self.ctx.terms.alloc_slice([Term::Variable(std_var)]),
3164                            world: None,
3165                        });
3166                        let comparison = self.ctx.exprs.alloc(LogicExpr::Comparative {
3167                            adjective: comp_adj,
3168                            subject: self.ctx.terms.alloc(subject_term.clone()),
3169                            object: self.ctx.terms.alloc(Term::Variable(std_var)),
3170                            difference: None,
3171                            relation: crate::ast::logic::ComparisonRelation::Greater,
3172                        });
3173                        let (std_kind, std_op) = match q {
3174                            TokenType::All => (QuantifierKind::Universal, TokenType::Implies),
3175                            TokenType::Most => (QuantifierKind::Most, TokenType::And),
3176                            TokenType::Few => (QuantifierKind::Few, TokenType::And),
3177                            TokenType::Many => (QuantifierKind::Many, TokenType::And),
3178                            _ => (QuantifierKind::Existential, TokenType::And),
3179                        };
3180                        let std_body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3181                            left: restriction,
3182                            op: std_op,
3183                            right: comparison,
3184                        });
3185                        let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
3186                            kind: std_kind,
3187                            variable: std_var,
3188                            body: std_body,
3189                            island_id: self.current_island,
3190                        });
3191                        self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3192                            left: event,
3193                            op: TokenType::And,
3194                            right: quantified,
3195                        })
3196                    } else {
3197                        // A descriptive standard ("faster than Quinn Quade's
3198                        // stamp", "faster than the cat that runs") becomes its own
3199                        // existential entity carrying its possessor/PP/relative
3200                        // clause, so it never collapses onto the subject; a bare
3201                        // name stays a constant.
3202                        let std_np = self.parse_noun_phrase(true)?;
3203                        let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
3204                        let is_desc = has_rel
3205                            || std_np.definiteness.is_some()
3206                            || !std_np.adjectives.is_empty()
3207                            || std_np.possessor.is_some()
3208                            || !std_np.pps.is_empty();
3209                        if is_desc {
3210                            let std_var = self.next_var_name();
3211                            let mut restr =
3212                                self.nominal_predication(Term::Variable(std_var), &std_np);
3213                            for pp in std_np.pps {
3214                                let pp_sub = self.substitute_pp_placeholder(pp, std_var);
3215                                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3216                                    left: restr,
3217                                    op: TokenType::And,
3218                                    right: pp_sub,
3219                                });
3220                            }
3221                            if has_rel {
3222                                self.advance(); // "who" / "that"
3223                                let rel = self.parse_relative_clause(std_var)?;
3224                                restr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3225                                    left: restr,
3226                                    op: TokenType::And,
3227                                    right: rel,
3228                                });
3229                            }
3230                            let comparison = self.ctx.exprs.alloc(LogicExpr::Comparative {
3231                                adjective: comp_adj,
3232                                subject: self.ctx.terms.alloc(subject_term.clone()),
3233                                object: self.ctx.terms.alloc(Term::Variable(std_var)),
3234                                difference: None,
3235                                relation: crate::ast::logic::ComparisonRelation::Greater,
3236                            });
3237                            let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3238                                left: restr,
3239                                op: TokenType::And,
3240                                right: comparison,
3241                            });
3242                            let quantified = self.ctx.exprs.alloc(LogicExpr::Quantifier {
3243                                kind: QuantifierKind::Existential,
3244                                variable: std_var,
3245                                body,
3246                                island_id: self.current_island,
3247                            });
3248                            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3249                                left: event,
3250                                op: TokenType::And,
3251                                right: quantified,
3252                            })
3253                        } else {
3254                            let comparison = self.ctx.exprs.alloc(LogicExpr::Comparative {
3255                                adjective: comp_adj,
3256                                subject: self.ctx.terms.alloc(subject_term.clone()),
3257                                object: self.ctx.terms.alloc(Term::Constant(std_np.noun)),
3258                                difference: None,
3259                                relation: crate::ast::logic::ComparisonRelation::Greater,
3260                            });
3261                            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3262                                left: event,
3263                                op: TokenType::And,
3264                                right: comparison,
3265                            })
3266                        }
3267                    };
3268                    return Ok(result);
3269                }
3270            }
3271
3272            // Perception small clause ("saw her duck", "watched the bird fly"):
3273            // a perception verb takes "NP bare-VP" naming the PERCEIVED event.
3274            // Gated on an actual Verb token so a Noun-variant parse of the same
3275            // word yields the distinct NP-object reading instead.
3276            if crate::lexicon::is_perception_verb(&self.interner.resolve(verb).to_lowercase()) {
3277                let mut vp_idx = None;
3278                let mut i = self.current;
3279                while i < self.tokens.len()
3280                    && !matches!(
3281                        self.tokens[i].kind,
3282                        TokenType::Period | TokenType::EOF | TokenType::Comma
3283                    )
3284                {
3285                    // Mode-aware verb reading: under noun priority an
3286                    // Ambiguous token takes its noun reading, so the small
3287                    // clause does not fire and the NP-object parse runs.
3288                    let is_verb_reading = match &self.tokens[i].kind {
3289                        TokenType::Verb { .. } => true,
3290                        TokenType::Ambiguous { primary, .. } if !self.noun_priority_mode => {
3291                            matches!(**primary, TokenType::Verb { .. })
3292                        }
3293                        _ => false,
3294                    };
3295                    if is_verb_reading {
3296                        vp_idx = Some(i);
3297                    }
3298                    i += 1;
3299                }
3300                if let Some(vp_i) = vp_idx {
3301                    if vp_i > self.current {
3302                        let psubj = match self.tokens[vp_i - 1].kind.clone() {
3303                            TokenType::Noun(n) | TokenType::ProperName(n) => Some(n),
3304                            TokenType::Pronoun { .. } | TokenType::Ambiguous { .. } => {
3305                                let lx = self
3306                                    .interner
3307                                    .resolve(self.tokens[vp_i - 1].lexeme)
3308                                    .to_lowercase();
3309                                let cap = lx
3310                                    .chars()
3311                                    .next()
3312                                    .map(|c| c.to_uppercase().collect::<String>() + &lx[1..])
3313                                    .unwrap_or(lx);
3314                                Some(self.interner.intern(&cap))
3315                            }
3316                            _ => None,
3317                        };
3318                        if let Some(psubj) = psubj {
3319                            let inner_verb = match &self.tokens[vp_i].kind {
3320                                TokenType::Verb { lemma, .. } => *lemma,
3321                                TokenType::Ambiguous { primary, .. } => {
3322                                    if let TokenType::Verb { lemma, .. } = **primary {
3323                                        lemma
3324                                    } else {
3325                                        unreachable!("gated on verb reading")
3326                                    }
3327                                }
3328                                _ => unreachable!("gated on verb reading"),
3329                            };
3330                            self.current = vp_i + 1; // consume through the VP head
3331                            let perceived = self.ctx.exprs.alloc(LogicExpr::Predicate {
3332                                name: inner_verb,
3333                                args: self.ctx.terms.alloc_slice([Term::Constant(psubj)]),
3334                                world: None,
3335                            });
3336                            let perceived_advs = self.collect_adverbs();
3337                            let perceived = if perceived_advs.is_empty() {
3338                                perceived
3339                            } else {
3340                                self.ctx.exprs.alloc(LogicExpr::Event {
3341                                    predicate: perceived,
3342                                    adverbs: self.ctx.syms.alloc_slice(perceived_advs),
3343                                })
3344                            };
3345                            let mut modifiers: Vec<Symbol> = Vec::new();
3346                            match verb_time {
3347                                Time::Past => modifiers.push(self.interner.intern("Past")),
3348                                Time::Future => modifiers.push(self.interner.intern("Future")),
3349                                _ => {}
3350                            }
3351                            let event_var = self.get_event_var();
3352                            let suppress_existential = self.drs.in_conditional_antecedent();
3353                            return Ok(self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(
3354                                NeoEventData {
3355                                    event_var,
3356                                    verb,
3357                                    roles: self.ctx.roles.alloc_slice(vec![
3358                                        (ThematicRole::Agent, subject_term.clone()),
3359                                        (ThematicRole::Theme, Term::Proposition(perceived)),
3360                                    ]),
3361                                    modifiers: self.ctx.syms.alloc_slice(modifiers),
3362                                    suppress_existential,
3363                                    world: None,
3364                                },
3365                            ))));
3366                        }
3367                    }
3368                }
3369            }
3370
3371            // Check for embedded wh-clause: "I know who/what"
3372            if self.check_wh_word() {
3373                let wh_token = self.advance().kind.clone();
3374
3375                let is_who = matches!(wh_token, TokenType::Who);
3376                let is_what = matches!(wh_token, TokenType::What);
3377
3378                // Check for sluicing: wh-word followed by terminator
3379                let is_sluicing = self.is_at_end() ||
3380                    self.check(&TokenType::Period) ||
3381                    self.check(&TokenType::Comma);
3382
3383                if is_sluicing {
3384                    if let Some(template) = self.last_event_template.clone() {
3385                        let wh_var = self.next_var_name();
3386
3387                        let roles: Vec<_> = if is_who {
3388                            std::iter::once((ThematicRole::Agent, Term::Variable(wh_var)))
3389                                .chain(template.non_agent_roles.iter().cloned())
3390                                .collect()
3391                        } else if is_what {
3392                            vec![
3393                                (ThematicRole::Agent, subject_term.clone()),
3394                                (ThematicRole::Theme, Term::Variable(wh_var)),
3395                            ]
3396                        } else {
3397                            std::iter::once((ThematicRole::Agent, Term::Variable(wh_var)))
3398                                .chain(template.non_agent_roles.iter().cloned())
3399                                .collect()
3400                        };
3401
3402                        let event_var = self.get_event_var();
3403                        let suppress_existential = self.drs.in_conditional_antecedent();
3404                        let reconstructed = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3405                            event_var,
3406                            verb: template.verb,
3407                            roles: self.ctx.roles.alloc_slice(roles),
3408                            modifiers: self.ctx.syms.alloc_slice(template.modifiers.clone()),
3409                            suppress_existential,
3410                            world: None,
3411                        })));
3412
3413                        let question = self.ctx.exprs.alloc(LogicExpr::Question {
3414                            wh_variable: wh_var,
3415                            body: reconstructed,
3416                        });
3417
3418                        let know_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3419                            event_var: self.get_event_var(),
3420                            verb,
3421                            roles: self.ctx.roles.alloc_slice(vec![
3422                                (ThematicRole::Agent, subject_term),
3423                                (ThematicRole::Theme, Term::Proposition(question)),
3424                            ]),
3425                            modifiers: self.ctx.syms.alloc_slice(vec![]),
3426                            suppress_existential,
3427                            world: None,
3428                        })));
3429
3430                        return Ok(know_event);
3431                    }
3432                }
3433
3434                // Non-sluicing: "I know who runs"
3435                let embedded = self.parse_embedded_wh_clause()?;
3436                let question = self.ctx.exprs.alloc(LogicExpr::Question {
3437                    wh_variable: self.interner.intern("x"),
3438                    body: embedded,
3439                });
3440
3441                let suppress_existential = self.drs.in_conditional_antecedent();
3442                let know_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3443                    event_var: self.get_event_var(),
3444                    verb,
3445                    roles: self.ctx.roles.alloc_slice(vec![
3446                        (ThematicRole::Agent, subject_term),
3447                        (ThematicRole::Theme, Term::Proposition(question)),
3448                    ]),
3449                    modifiers: self.ctx.syms.alloc_slice(vec![]),
3450                    suppress_existential,
3451                    world: None,
3452                })));
3453
3454                return Ok(know_event);
3455            }
3456
3457            // Opaque attitude verbs take a finite clausal complement as a STRUCTURED
3458            // PROPOSITION (P3), not an extensional object: "John believes Mary left."
3459            // → Believe(John, ⟨Left(Mary)⟩). A pure-token lookahead detects an
3460            // embedded proper-name/pronoun subject directly followed by a verb
3461            // (optionally after the complementizer "that"); article-headed embedded
3462            // clauses ("a spy exists") are already handled downstream and untouched.
3463            if crate::lexicon::is_opaque_verb(&self.interner.resolve(verb).to_lowercase()) {
3464                let mut i = self.current;
3465                if i < self.tokens.len() && matches!(self.tokens[i].kind, TokenType::That) {
3466                    i += 1;
3467                }
3468                let subj_is_name_or_pronoun = i < self.tokens.len()
3469                    && matches!(
3470                        self.tokens[i].kind,
3471                        TokenType::ProperName(_) | TokenType::Pronoun { .. }
3472                    );
3473                let verb_follows = subj_is_name_or_pronoun
3474                    && i + 1 < self.tokens.len()
3475                    && matches!(
3476                        self.tokens[i + 1].kind,
3477                        TokenType::Verb { .. } | TokenType::Auxiliary(_)
3478                    );
3479                // Article-headed embedded subject with a finite clause:
3480                // "believes that THE TEACHER wants …". (Indefinite objects
3481                // without a following verb keep the de re/de dicto path.)
3482                let definite_np_clause = i + 2 < self.tokens.len()
3483                    && matches!(self.tokens[i].kind, TokenType::Article(_))
3484                    && matches!(self.tokens[i + 1].kind, TokenType::Noun(_))
3485                    && matches!(
3486                        self.tokens[i + 2].kind,
3487                        TokenType::Verb { .. } | TokenType::Auxiliary(_)
3488                    );
3489                if verb_follows || definite_np_clause {
3490                    if self.check(&TokenType::That) {
3491                        self.advance();
3492                    }
3493                    let embedded_subject = match self.peek().kind {
3494                        TokenType::ProperName(s) => {
3495                            self.advance();
3496                            s
3497                        }
3498                        TokenType::Pronoun { gender, number, .. } => {
3499                            self.advance();
3500                            match self.resolve_pronoun(gender, number)? {
3501                                super::ResolvedPronoun::Variable(s)
3502                                | super::ResolvedPronoun::Constant(s) => s,
3503                            }
3504                        }
3505                        TokenType::Article(_) => {
3506                            let np = self.parse_noun_phrase(false)?;
3507                            np.noun
3508                        }
3509                        _ => unreachable!("guarded by subj_is_name_or_pronoun"),
3510                    };
3511                    let embedded_pred = self.parse_predicate_with_subject(embedded_subject)?;
3512                    let embedded_term = Term::Proposition(embedded_pred);
3513                    let main_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
3514                        name: verb,
3515                        args: self
3516                            .ctx
3517                            .terms
3518                            .alloc_slice([subject_term.clone(), embedded_term]),
3519                        world: None,
3520                    });
3521                    let effective_time = self.pending_time.take().unwrap_or(verb_time);
3522                    return Ok(if effective_time == Time::Past {
3523                        self.ctx.exprs.alloc(LogicExpr::Temporal {
3524                            operator: TemporalOperator::Past,
3525                            body: main_pred,
3526                        })
3527                    } else {
3528                        main_pred
3529                    });
3530                }
3531            }
3532
3533            let mut object_term: Option<Term<'a>> = None;
3534            let mut second_object_term: Option<Term<'a>> = None;
3535            // "X has Y as [its] ROLE" — a predicative secondary naming the role Y
3536            // fills (set after the object is parsed; conjoined as Role(Y) below).
3537            let mut as_role: Option<Symbol> = None;
3538            // A filler-gap object licenses a stranded preposition ("Who did John talk to?").
3539            let mut gap_object = false;
3540            let mut object_pps: &[&LogicExpr<'a>] = &[];  // PPs attached to object NP (for NP-attachment mode)
3541            let mut object_adjectives: &[Symbol] = &[];   // adjectives on a definite/constant object ("the GREEN shirt")
3542            if self.check(&TokenType::Reflexive) {
3543                self.advance();
3544                // The reflexive binds the subject TERM, preserving its
3545                // variable-ness under a quantified subject ("Every man loves
3546                // himself." → Theme(e, x), not a constant).
3547                let term = subject_term.clone();
3548                object_term = Some(term.clone());
3549                args.push(term);
3550            } else if self.check_pronoun()
3551                && !(self.check_possessive_pronoun()
3552                    && match self.tokens.get(self.current + 1).map(|t| t.kind.clone()) {
3553                        Some(TokenType::Noun(_)) => true,
3554                        // Under noun priority an Ambiguous next token reads as
3555                        // a noun, so "her duck" is a possessive NP object.
3556                        Some(TokenType::Ambiguous { .. }) => self.noun_priority_mode,
3557                        _ => false,
3558                    })
3559            {
3560                let token = self.advance().clone();
3561                let (gender, number) = match &token.kind {
3562                    TokenType::Pronoun { gender, number, .. } => (*gender, *number),
3563                    TokenType::Ambiguous { primary, alternatives } => {
3564                        if let TokenType::Pronoun { gender, number, .. } = **primary {
3565                            (gender, number)
3566                        } else {
3567                            alternatives.iter().find_map(|t| {
3568                                if let TokenType::Pronoun { gender, number, .. } = t {
3569                                    Some((*gender, *number))
3570                                } else {
3571                                    None
3572                                }
3573                            }).unwrap_or((Gender::Unknown, Number::Singular))
3574                        }
3575                    }
3576                    _ => (Gender::Unknown, Number::Singular),
3577                };
3578
3579                // Person deictics (§8.4) resolve to the discourse roles in any
3580                // position: object "you" → Addressee, "me" → Speaker.
3581                let plex = self.interner.resolve(token.lexeme).to_lowercase();
3582                let term = match plex.as_str() {
3583                    "you" | "yourself" => Term::Constant(self.interner.intern("Addressee")),
3584                    "me" | "myself" | "i" => Term::Constant(self.interner.intern("Speaker")),
3585                    // A donkey antecedent (indefinite from a quantifier's
3586                    // restriction) outranks discourse resolution: "Every man
3587                    // who owns a book reads it." → Theme(e, y).
3588                    _ => match self.resolve_donkey_pronoun(gender) {
3589                        Some(donkey_var) => Term::Variable(donkey_var),
3590                        None => match self.resolve_pronoun(gender, number)? {
3591                            super::ResolvedPronoun::Variable(s) => Term::Variable(s),
3592                            super::ResolvedPronoun::Constant(s) => Term::Constant(s),
3593                        },
3594                    },
3595                };
3596                object_term = Some(term);
3597                args.push(term);
3598
3599                let verb_str = self.interner.resolve(verb);
3600                if Lexer::is_ditransitive_verb(verb_str)
3601                    && (self.check_content_word() || self.check_article())
3602                {
3603                    let second_np = self.parse_noun_phrase(false)?;
3604                    let second_term = Term::Constant(second_np.noun);
3605                    second_object_term = Some(second_term);
3606                    args.push(second_term);
3607                }
3608            } else if self.peek_definite_reduced_relative_object() {
3609                // A definite object whose head carries a REDUCED OBJECT RELATIVE
3610                // ("Determine the friend Simon went with"). Parse the WHOLE NP —
3611                // article included — so `parse_noun_phrase` runs its reduced-relative
3612                // machinery and returns the relative as a `_PP_SELF_` PP over the
3613                // head. Pre-consuming the article (the generic definite path) would
3614                // hide the determiner and strand the relative's verb. The head noun
3615                // and the relative both survive: the PP is rebound to the object
3616                // constant by the shared object-PP application below. Parsed GREEDILY
3617                // so the reduced relative — a genuine head restrictor, gated above the
3618                // non-greedy PP cutoff — attaches; the precise dispatch guard keeps
3619                // this from over-consuming any other construction.
3620                self.nominal_np_context = true;
3621                let object_np_result = self.parse_noun_phrase(true);
3622                self.nominal_np_context = false;
3623                let object_np = object_np_result?;
3624
3625                let obj_gender = Self::infer_noun_gender(self.interner.resolve(object_np.noun));
3626                let obj_number = if Self::is_plural_noun(self.interner.resolve(object_np.noun)) {
3627                    Number::Plural
3628                } else {
3629                    Number::Singular
3630                };
3631                self.drs.introduce_referent_with_source(
3632                    object_np.noun,
3633                    object_np.noun,
3634                    obj_gender,
3635                    obj_number,
3636                    ReferentSource::MainClause,
3637                );
3638
3639                let term = Term::Constant(object_np.noun);
3640                object_term = Some(term);
3641                object_pps = object_np.pps;
3642                object_adjectives = object_np.adjectives;
3643                args.push(term);
3644            } else if self.counting_np_lookahead().is_some()
3645                || self.check_quantifier()
3646                || self.check_article()
3647                || self.check_possessive_pronoun()
3648            {
3649                let obj_quantifier = if let Some(n) = self.counting_np_lookahead() {
3650                    // A digit-led counting NP object ("saw 6 brown manatees"):
3651                    // consume the integer and quantify the remaining
3652                    // "(adjective)+ noun" as ∃=n, reusing the canonical
3653                    // quantified-object construction below. Without this the
3654                    // adjective is mis-read as a measure unit (→ a bogus
3655                    // Recipient role and a dropped count).
3656                    self.advance();
3657                    Some(TokenType::Cardinal(n))
3658                } else if self.check_possessive_pronoun() {
3659                    // Possessive NP object ("his dog"): parse_noun_phrase
3660                    // consumes the possessor; no quantifier wrapper.
3661                    None
3662                } else if self.check_quantifier() {
3663                    Some(self.advance().kind.clone())
3664                } else {
3665                    let art = self.advance().kind.clone();
3666                    if let TokenType::Article(def) = art {
3667                        if def == Definiteness::Indefinite {
3668                            Some(TokenType::Some)
3669                        } else {
3670                            None
3671                        }
3672                    } else {
3673                        None
3674                    }
3675                };
3676
3677                // The quantifier/article/cardinal just established this as an NP,
3678                // so a verb-word head after an adjective is a deverbal noun.
3679                self.nominal_np_context = true;
3680                let object_np_result = self.parse_noun_phrase(false);
3681                self.nominal_np_context = false;
3682                let object_np = object_np_result?;
3683
3684                if let Some(obj_q) = obj_quantifier {
3685                    let obj_var = self.next_var_name();
3686
3687                    // Introduce object referent in DRS for cross-sentence anaphora
3688                    let obj_gender = Self::infer_noun_gender(self.interner.resolve(object_np.noun));
3689                    let obj_number = if Self::is_plural_noun(self.interner.resolve(object_np.noun)) {
3690                        Number::Plural
3691                    } else {
3692                        Number::Singular
3693                    };
3694                    // Definite descriptions presuppose existence, so they should be globally accessible
3695                    if object_np.definiteness == Some(Definiteness::Definite) {
3696                        self.drs.introduce_referent_with_source(obj_var, object_np.noun, obj_gender, obj_number, ReferentSource::MainClause);
3697                    } else {
3698                        self.drs.introduce_referent(obj_var, object_np.noun, obj_gender, obj_number);
3699                    }
3700
3701                    let mut obj_restriction: &'a LogicExpr<'a> =
3702                        self.ctx.exprs.alloc(LogicExpr::Predicate {
3703                            name: object_np.noun,
3704                            args: self.ctx.terms.alloc_slice([Term::Variable(obj_var)]),
3705                            world: None,
3706                        });
3707                    // A quantified object's own restrictors — adjectives ("a RED
3708                    // book") and PPs ("a maximum range OF 475 ft" → Range(o) ∧
3709                    // Of(o, 475 ft)) — must survive; dropping them is meaning loss.
3710                    for &adj in object_np.adjectives {
3711                        let adj_pred = self.adjective_restriction(adj, obj_var, object_np.noun);
3712                        obj_restriction = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3713                            left: obj_restriction,
3714                            op: TokenType::And,
3715                            right: adj_pred,
3716                        });
3717                    }
3718                    for pp in object_np.pps {
3719                        let pp_sub = self.substitute_pp_placeholder(pp, obj_var);
3720                        obj_restriction = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3721                            left: obj_restriction,
3722                            op: TokenType::And,
3723                            right: pp_sub,
3724                        });
3725                    }
3726
3727                    // Continuations inside the object quantifier's scope: a
3728                    // second object ("gave some student a book"), a recipient
3729                    // ("gave a book to Mary", "… to some teacher"), or an
3730                    // object-control infinitive ("caused all flowers to bloom").
3731                    let verb_str = self.interner.resolve(verb).to_string();
3732                    let mut second_object: Option<Term<'a>> = None;
3733                    let mut recipient: Option<Term<'a>> = None;
3734                    let mut recipient_quant: Option<(TokenType, Symbol, Symbol)> = None;
3735                    let mut control_infinitive: Option<Symbol> = None;
3736
3737                    if Lexer::is_ditransitive_verb(&verb_str)
3738                        && (self.check_content_word() || self.check_article())
3739                    {
3740                        let second_np = self.parse_noun_phrase(false)?;
3741                        second_object = Some(Term::Constant(second_np.noun));
3742                    } else if self.check_to_marker() {
3743                        let after_to = self.tokens.get(self.current + 1).map(|t| t.kind.clone());
3744                        match after_to {
3745                            Some(TokenType::Verb { lemma, .. }) => {
3746                                self.advance(); // to
3747                                self.advance(); // infinitive verb
3748                                control_infinitive = Some(lemma);
3749                            }
3750                            // After a preposition the lexer may classify the
3751                            // infinitive as a noun ("to bloom"); the lexicon
3752                            // recovers the verb reading.
3753                            Some(TokenType::Noun(word))
3754                                if crate::lexicon::lookup_verb_db(
3755                                    &self.interner.resolve(word).to_lowercase(),
3756                                )
3757                                .is_some() =>
3758                            {
3759                                let lemma_str = crate::lexicon::lookup_verb_db(
3760                                    &self.interner.resolve(word).to_lowercase(),
3761                                )
3762                                .map(|m| m.lemma)
3763                                .unwrap();
3764                                self.advance(); // to
3765                                self.advance(); // infinitive verb (noun-classified)
3766                                control_infinitive = Some(self.interner.intern(lemma_str));
3767                            }
3768                            Some(kind)
3769                                if Lexer::is_ditransitive_verb(&verb_str)
3770                                    && matches!(
3771                                        kind,
3772                                        TokenType::All
3773                                            | TokenType::Some
3774                                            | TokenType::No
3775                                            | TokenType::Most
3776                                            | TokenType::Few
3777                                            | TokenType::Many
3778                                            | TokenType::Cardinal(_)
3779                                            | TokenType::AtLeast(_)
3780                                            | TokenType::AtMost(_)
3781                                    ) =>
3782                            {
3783                                self.advance(); // to
3784                                let r_quant = self.advance().kind.clone();
3785                                let r_np = self.parse_noun_phrase(false)?;
3786                                let r_var = self.next_var_name();
3787                                recipient_quant = Some((r_quant, r_var, r_np.noun));
3788                            }
3789                            Some(kind)
3790                                if Lexer::is_ditransitive_verb(&verb_str)
3791                                    && matches!(
3792                                        kind,
3793                                        TokenType::ProperName(_)
3794                                            | TokenType::Noun(_)
3795                                            | TokenType::Article(_)
3796                                    ) =>
3797                            {
3798                                self.advance(); // to
3799                                let r_np = self.parse_noun_phrase(false)?;
3800                                recipient = Some(Term::Constant(r_np.noun));
3801                            }
3802                            _ => {}
3803                        }
3804                    }
3805
3806                    let event_var = self.get_event_var();
3807                    let mut modifiers = self.collect_adverbs();
3808                    let effective_time = self.pending_time.take().unwrap_or(verb_time);
3809                    match effective_time {
3810                        Time::Past => modifiers.push(self.interner.intern("Past")),
3811                        Time::Future => modifiers.push(self.interner.intern("Future")),
3812                        _ => {}
3813                    }
3814
3815                    let mut roles = vec![(ThematicRole::Agent, subject_term.clone())];
3816                    if let Some(second) = second_object {
3817                        roles.push((ThematicRole::Recipient, Term::Variable(obj_var)));
3818                        roles.push((ThematicRole::Theme, second));
3819                    } else {
3820                        roles.push((ThematicRole::Theme, Term::Variable(obj_var)));
3821                        if let Some(r) = recipient {
3822                            roles.push((ThematicRole::Recipient, r));
3823                        } else if let Some((_, r_var, _)) = recipient_quant {
3824                            roles.push((ThematicRole::Recipient, Term::Variable(r_var)));
3825                        }
3826                    }
3827
3828                    let suppress_existential = self.drs.in_conditional_antecedent();
3829                    let neo_event = if let Some(inf) = control_infinitive {
3830                        let inf_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
3831                            name: inf,
3832                            args: self.ctx.terms.alloc_slice([Term::Variable(obj_var)]),
3833                            world: None,
3834                        });
3835                        let control = self.ctx.exprs.alloc(LogicExpr::Control {
3836                            verb,
3837                            subject: self.ctx.terms.alloc(subject_term.clone()),
3838                            object: Some(&*self.ctx.terms.alloc(Term::Variable(obj_var))),
3839                            infinitive: inf_pred,
3840                        });
3841                        match effective_time {
3842                            Time::Past => &*self.ctx.exprs.alloc(LogicExpr::Temporal {
3843                                operator: TemporalOperator::Past,
3844                                body: control,
3845                            }),
3846                            Time::Future => &*self.ctx.exprs.alloc(LogicExpr::Temporal {
3847                                operator: TemporalOperator::Future,
3848                                body: control,
3849                            }),
3850                            _ => control,
3851                        }
3852                    } else {
3853                        let plain = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
3854                            event_var,
3855                            verb,
3856                            roles: self.ctx.roles.alloc_slice(roles),
3857                            modifiers: self.ctx.syms.alloc_slice(modifiers),
3858                            suppress_existential,
3859                            world: None,
3860                        })));
3861                        if let Some((r_quant, r_var, r_noun)) = recipient_quant {
3862                            let r_restriction = self.ctx.exprs.alloc(LogicExpr::Predicate {
3863                                name: r_noun,
3864                                args: self.ctx.terms.alloc_slice([Term::Variable(r_var)]),
3865                                world: None,
3866                            });
3867                            let r_kind = match r_quant {
3868                                TokenType::All => QuantifierKind::Universal,
3869                                TokenType::Most => QuantifierKind::Most,
3870                                TokenType::Few => QuantifierKind::Few,
3871                                TokenType::Many => QuantifierKind::Many,
3872                                TokenType::Cardinal(n) => QuantifierKind::Cardinal(n),
3873                                TokenType::AtLeast(n) => QuantifierKind::AtLeast(n),
3874                                TokenType::AtMost(n) => QuantifierKind::AtMost(n),
3875                                _ => QuantifierKind::Existential,
3876                            };
3877                            let r_body = if matches!(r_kind, QuantifierKind::Universal) {
3878                                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3879                                    left: r_restriction,
3880                                    op: TokenType::Implies,
3881                                    right: plain,
3882                                })
3883                            } else {
3884                                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3885                                    left: r_restriction,
3886                                    op: TokenType::And,
3887                                    right: plain,
3888                                })
3889                            };
3890                            &*self.ctx.exprs.alloc(LogicExpr::Quantifier {
3891                                kind: r_kind,
3892                                variable: r_var,
3893                                body: r_body,
3894                                island_id: self.current_island,
3895                            })
3896                        } else {
3897                            plain
3898                        }
3899                    };
3900
3901                    // Trailing event-PP adjuncts ("takes a holiday WITH a friend TO
3902                    // some location") attach to the event INSIDE the object's
3903                    // existential scope — the non-quantified path does this inline.
3904                    let neo_event = self.attach_trailing_event_pps(neo_event, event_var)?;
3905
3906                    let obj_kind = match obj_q {
3907                        TokenType::All => QuantifierKind::Universal,
3908                        TokenType::Some => QuantifierKind::Existential,
3909                        TokenType::No => QuantifierKind::Universal,
3910                        TokenType::Most => QuantifierKind::Most,
3911                        TokenType::Few => QuantifierKind::Few,
3912                        TokenType::Many => QuantifierKind::Many,
3913                        TokenType::Cardinal(n) => QuantifierKind::Cardinal(n),
3914                        TokenType::AtLeast(n) => QuantifierKind::AtLeast(n),
3915                        TokenType::AtMost(n) => QuantifierKind::AtMost(n),
3916                        _ => QuantifierKind::Existential,
3917                    };
3918
3919                    let obj_body = match obj_q {
3920                        TokenType::All => self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3921                            left: obj_restriction,
3922                            op: TokenType::Implies,
3923                            right: neo_event,
3924                        }),
3925                        TokenType::No => {
3926                            let neg = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
3927                                op: TokenType::Not,
3928                                operand: neo_event,
3929                            });
3930                            self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3931                                left: obj_restriction,
3932                                op: TokenType::Implies,
3933                                right: neg,
3934                            })
3935                        }
3936                        _ => self.ctx.exprs.alloc(LogicExpr::BinaryOp {
3937                            left: obj_restriction,
3938                            op: TokenType::And,
3939                            right: neo_event,
3940                        }),
3941                    };
3942
3943                    return Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
3944                        kind: obj_kind,
3945                        variable: obj_var,
3946                        body: obj_body,
3947                        island_id: self.current_island,
3948                    }));
3949                } else {
3950                    // Definite object NP (e.g., "the house")
3951                    // Introduce to DRS for cross-sentence bridging anaphora
3952                    // E.g., "John entered the house. The door was open." - door bridges to house
3953                    if object_np.definiteness == Some(Definiteness::Definite) {
3954                        let obj_gender = Self::infer_noun_gender(self.interner.resolve(object_np.noun));
3955                        let obj_number = if Self::is_plural_noun(self.interner.resolve(object_np.noun)) {
3956                            Number::Plural
3957                        } else {
3958                            Number::Singular
3959                        };
3960                        // Definite descriptions presuppose existence, so they should be globally accessible
3961                        self.drs.introduce_referent_with_source(object_np.noun, object_np.noun, obj_gender, obj_number, ReferentSource::MainClause);
3962                    }
3963
3964                    let term = Term::Constant(object_np.noun);
3965                    object_term = Some(term);
3966                    // Store the definite object's adjectives + PPs so they are
3967                    // predicated of the object ("ate the RED apple" → Red(Apple));
3968                    // dropping them is a meaning-loss parse.
3969                    object_pps = object_np.pps;
3970                    object_adjectives = object_np.adjectives;
3971                    args.push(term);
3972
3973                    // Ditransitive with a DEFINITE indirect object: "gave the
3974                    // winner the prize" — the definite IO took this non-quantified
3975                    // branch (a definite article carries no obj_quantifier), so the
3976                    // direct object must still be picked up here, mirroring the
3977                    // proper-name/quantified IO paths. The double-object builder
3978                    // then assigns Recipient(IO) ∧ Theme(DO); without this the DO
3979                    // strands as a trailing token.
3980                    let verb_str = self.interner.resolve(verb);
3981                    if Lexer::is_ditransitive_verb(verb_str)
3982                        && (self.check_content_word() || self.check_article())
3983                    {
3984                        let second_np = self.parse_noun_phrase(false)?;
3985                        let second_term = Term::Constant(second_np.noun);
3986                        second_object_term = Some(second_term);
3987                        args.push(second_term);
3988                    }
3989                }
3990            } else if self.check_focus() {
3991                let focus_kind = if let TokenType::Focus(k) = self.advance().kind {
3992                    k
3993                } else {
3994                    FocusKind::Only
3995                };
3996
3997                let event_var = self.get_event_var();
3998                let mut modifiers = self.collect_adverbs();
3999                let effective_time = self.pending_time.take().unwrap_or(verb_time);
4000                match effective_time {
4001                    Time::Past => modifiers.push(self.interner.intern("Past")),
4002                    Time::Future => modifiers.push(self.interner.intern("Future")),
4003                    _ => {}
4004                }
4005
4006                if self.check_preposition() {
4007                    let prep_token = self.advance().clone();
4008                    let prep_name = if let TokenType::Preposition(sym) = prep_token.kind {
4009                        sym
4010                    } else {
4011                        self.interner.intern("to")
4012                    };
4013                    let pp_obj = self.parse_noun_phrase(false)?;
4014                    let pp_obj_term = Term::Constant(pp_obj.noun);
4015
4016                    let roles = vec![(ThematicRole::Agent, subject_term)];
4017                    let suppress_existential = self.drs.in_conditional_antecedent();
4018                    let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
4019                        event_var,
4020                        verb,
4021                        roles: self.ctx.roles.alloc_slice(roles),
4022                        modifiers: self.ctx.syms.alloc_slice(modifiers),
4023                        suppress_existential,
4024                        world: None,
4025                    })));
4026
4027                    let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4028                        name: prep_name,
4029                        args: self.ctx.terms.alloc_slice([Term::Variable(event_var), pp_obj_term]),
4030                        world: None,
4031                    });
4032
4033                    let with_pp = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4034                        left: neo_event,
4035                        op: TokenType::And,
4036                        right: pp_pred,
4037                    });
4038
4039                    let focused_ref = self.ctx.terms.alloc(pp_obj_term);
4040                    return Ok(self.ctx.exprs.alloc(LogicExpr::Focus {
4041                        kind: focus_kind,
4042                        focused: focused_ref,
4043                        scope: with_pp,
4044                    }));
4045                }
4046
4047                let focused_np = self.parse_noun_phrase(false)?;
4048                let focused_term = Term::Constant(focused_np.noun);
4049                args.push(focused_term);
4050
4051                let roles = vec![
4052                    (ThematicRole::Agent, subject_term),
4053                    (ThematicRole::Theme, focused_term),
4054                ];
4055
4056                let suppress_existential = self.drs.in_conditional_antecedent();
4057                let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
4058                    event_var,
4059                    verb,
4060                    roles: self.ctx.roles.alloc_slice(roles),
4061                    modifiers: self.ctx.syms.alloc_slice(modifiers),
4062                    suppress_existential,
4063                    world: None,
4064                })));
4065
4066                let focused_ref = self.ctx.terms.alloc(focused_term);
4067                return Ok(self.ctx.exprs.alloc(LogicExpr::Focus {
4068                    kind: focus_kind,
4069                    focused: focused_ref,
4070                    scope: neo_event,
4071                }));
4072            } else if self.check(&TokenType::Either) {
4073                // "danced either the hustle or the lindy" → Dance(S,Hustle) ∨ Dance(S,Lindy)
4074                self.advance(); // consume "either"
4075                let np1 = self.parse_noun_phrase(true)?;
4076                if self.check(&TokenType::Or) {
4077                    self.advance(); // consume "or"
4078                    let np2 = self.parse_noun_phrase(true)?;
4079                    let effective_time = self.pending_time.take().unwrap_or(verb_time);
4080                    let placeholder = self.interner.intern("_PP_SELF_");
4081                    let possesses = self.interner.intern("Possesses");
4082                    // Build a disjunct's predication, KEEPING the object NP's
4083                    // possessor and PPs ("either the hustle from Spain or …"
4084                    // must not drop "from Spain"; "either Tara's routine or …"
4085                    // must not drop the possessor).
4086                    let mut build = |p: &mut Self, np: &NounPhrase<'a>| -> &'a LogicExpr<'a> {
4087                        let obj = Term::Constant(np.noun);
4088                        let mut pred: &'a LogicExpr<'a> = p.ctx.exprs.alloc(LogicExpr::Predicate {
4089                            name: verb,
4090                            args: p.ctx.terms.alloc_slice([subject_term, obj]),
4091                            world: None,
4092                        });
4093                        if let Some(possessor) = np.possessor {
4094                            let poss = p.ctx.exprs.alloc(LogicExpr::Predicate {
4095                                name: possesses,
4096                                args: p
4097                                    .ctx
4098                                    .terms
4099                                    .alloc_slice([Term::Constant(possessor.noun), obj]),
4100                                world: None,
4101                            });
4102                            pred = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
4103                                left: pred,
4104                                op: TokenType::And,
4105                                right: poss,
4106                            });
4107                        }
4108                        for pp in np.pps {
4109                            let pp_sub = match pp {
4110                                LogicExpr::Predicate { name, args, world } => {
4111                                    let new_args: Vec<Term<'a>> = args
4112                                        .iter()
4113                                        .map(|a| match a {
4114                                            Term::Variable(v) if *v == placeholder => obj,
4115                                            other => *other,
4116                                        })
4117                                        .collect();
4118                                    p.ctx.exprs.alloc(LogicExpr::Predicate {
4119                                        name: *name,
4120                                        args: p.ctx.terms.alloc_slice(new_args),
4121                                        world: *world,
4122                                    })
4123                                }
4124                                other => *other,
4125                            };
4126                            pred = p.ctx.exprs.alloc(LogicExpr::BinaryOp {
4127                                left: pred,
4128                                op: TokenType::And,
4129                                right: pp_sub,
4130                            });
4131                        }
4132                        pred
4133                    };
4134                    let pred1 = build(self, &np1);
4135                    let pred2 = build(self, &np2);
4136                    let mut wrap_time = |p: &mut Self, e: &'a LogicExpr<'a>| -> &'a LogicExpr<'a> {
4137                        if effective_time == Time::Past {
4138                            p.ctx.exprs.alloc(LogicExpr::Temporal {
4139                                operator: TemporalOperator::Past,
4140                                body: e,
4141                            })
4142                        } else {
4143                            e
4144                        }
4145                    };
4146                    let pred1 = wrap_time(self, pred1);
4147                    let pred2 = wrap_time(self, pred2);
4148                    return Ok(self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4149                        left: pred1,
4150                        op: TokenType::Or,
4151                        right: pred2,
4152                    }));
4153                }
4154                // No "or" — fall through with np1 as plain object
4155                let term = Term::Constant(np1.noun);
4156                object_term = Some(term);
4157                args.push(term);
4158            } else if self.check_number() {
4159                let measure = self.parse_measure_phrase()?;
4160                // The measured quantity is the verb's Theme ("scored 190 points"
4161                // → Theme(e, 190 points)); without this the count is parsed but
4162                // never reaches a thematic role and the number is lost.
4163                object_term = Some(*measure);
4164                if self.check_content_word() {
4165                    let noun_sym = self.consume_content_word()?;
4166                    args.push(*measure);
4167                    args.push(Term::Constant(noun_sym));
4168                } else {
4169                    args.push(*measure);
4170                }
4171            } else if self.check_content_word() {
4172                let potential_object = self.parse_noun_phrase(false)?;
4173                // Store the object's adjectives + PPs for NP-attachment mode
4174                object_pps = potential_object.pps;
4175                object_adjectives = potential_object.adjectives;
4176
4177                // A finite clausal complement (the NP is followed by a verb) is taken
4178                // as a structured proposition (P3) when the matrix verb is an opaque
4179                // attitude verb ("John believes Mary left." → Believe(John, ⟨Left(Mary)⟩))
4180                // or in a filler-gap context. The complement keeps its own structure
4181                // so co-intensional complements stay distinct and substitution into it
4182                // is blocked.
4183                let verb_is_opaque =
4184                    crate::lexicon::is_opaque_verb(&self.interner.resolve(verb).to_lowercase());
4185                if self.check_verb() && (self.filler_gap.is_some() || verb_is_opaque) {
4186                    let embedded_subject = potential_object.noun;
4187                    let embedded_pred = self.parse_predicate_with_subject(embedded_subject)?;
4188
4189                    let embedded_term = Term::Proposition(embedded_pred);
4190                    let main_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4191                        name: verb,
4192                        args: self.ctx.terms.alloc_slice([subject_term, embedded_term]),
4193                        world: None,
4194                    });
4195
4196                    let effective_time = self.pending_time.take().unwrap_or(verb_time);
4197                    return Ok(if effective_time == Time::Past {
4198                        self.ctx.exprs.alloc(LogicExpr::Temporal {
4199                            operator: TemporalOperator::Past,
4200                            body: main_pred,
4201                        })
4202                    } else {
4203                        main_pred
4204                    });
4205                }
4206
4207                // Collect all objects for potential "respectively" handling
4208                let mut all_objects: Vec<Symbol> = vec![potential_object.noun];
4209
4210                // Check for coordinated objects: "Tom and Jerry and Bob"
4211                while self.check(&TokenType::And) {
4212                    let saved = self.current;
4213                    self.advance(); // consume "and"
4214                    if self.check_content_word() || self.check_article() {
4215                        let next_obj = match self.parse_noun_phrase(false) {
4216                            Ok(np) => np,
4217                            Err(_) => {
4218                                self.current = saved;
4219                                break;
4220                            }
4221                        };
4222                        all_objects.push(next_obj.noun);
4223                    } else {
4224                        self.current = saved;
4225                        break;
4226                    }
4227                }
4228
4229                // Check for "respectively" with single subject
4230                if self.check(&TokenType::Respectively) {
4231                    let respectively_span = self.peek().span;
4232                    // Single subject with multiple objects + respectively = error
4233                    if all_objects.len() > 1 {
4234                        return Err(ParseError {
4235                            kind: ParseErrorKind::RespectivelyLengthMismatch {
4236                                subject_count: 1,
4237                                object_count: all_objects.len(),
4238                            },
4239                            span: respectively_span,
4240                        });
4241                    }
4242                    // Single subject, single object + respectively is valid (trivially pairwise)
4243                    self.advance(); // consume "respectively"
4244                }
4245
4246                // Use the first object (or only object) for normal processing
4247                let term = Term::Constant(all_objects[0]);
4248                object_term = Some(term);
4249                args.push(term);
4250
4251                // For multiple objects without "respectively", use group semantics
4252                if all_objects.len() > 1 {
4253                    let obj_members: Vec<Term<'a>> = all_objects.iter()
4254                        .map(|o| Term::Constant(*o))
4255                        .collect();
4256                    let obj_group = Term::Group(self.ctx.terms.alloc_slice(obj_members));
4257                    // Replace the single object with the group — both in the
4258                    // predicate args AND as the Theme term, so every coordinate
4259                    // survives the neo-event role assignment (the Theme reads
4260                    // `object_term`; leaving it on the first member silently drops
4261                    // the rest, e.g. "year" in "the activity, state and year").
4262                    args.pop();
4263                    args.push(obj_group);
4264                    object_term = Some(obj_group);
4265                }
4266
4267                let verb_str = self.interner.resolve(verb);
4268                if Lexer::is_ditransitive_verb(verb_str)
4269                    && (self.check_content_word() || self.check_article())
4270                {
4271                    let second_np = self.parse_noun_phrase(false)?;
4272                    let second_term = Term::Constant(second_np.noun);
4273                    second_object_term = Some(second_term);
4274                    args.push(second_term);
4275                }
4276            } else if self.filler_gap.is_some() && !self.check_content_word() && !self.check_pronoun()
4277            {
4278                let gap_var = self.filler_gap.take().unwrap();
4279                let term = Term::Variable(gap_var);
4280                object_term = Some(term);
4281                args.push(term);
4282                gap_object = true;
4283            }
4284
4285            let unknown = self.interner.intern("?");
4286            let mut pp_predicates: Vec<&'a LogicExpr<'a>> = Vec::new();
4287
4288            // Check for distanced phrasal verb particle: "gave the book up"
4289            if let TokenType::Particle(particle_sym) = self.peek().kind {
4290                let verb_str = self.interner.resolve(verb).to_lowercase();
4291                let particle_str = self.interner.resolve(particle_sym).to_lowercase();
4292                if let Some((phrasal_lemma, _class)) = crate::lexicon::lookup_phrasal_verb(&verb_str, &particle_str) {
4293                    self.advance(); // consume the particle
4294                    verb = self.interner.intern(phrasal_lemma);
4295                } else {
4296                    // A particle with no phrasal-verb table entry ("came OUT",
4297                    // "went UP") — keep it as a particle predicate over the event so
4298                    // a trailing PP still attaches ("came out IN 1995"). The
4299                    // clause-final case is handled in the PP loop; this covers the
4300                    // particle-then-PP case it misses.
4301                    self.advance(); // consume the particle
4302                    let event_sym = self.get_event_var();
4303                    let cap = {
4304                        let p = self.interner.resolve(particle_sym);
4305                        let mut chs = p.chars();
4306                        match chs.next() {
4307                            Some(f) => f.to_uppercase().collect::<String>() + chs.as_str(),
4308                            None => String::new(),
4309                        }
4310                    };
4311                    pp_predicates.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
4312                        name: self.interner.intern(&cap),
4313                        args: self.ctx.terms.alloc_slice([Term::Variable(event_sym)]),
4314                        world: None,
4315                    }));
4316                }
4317            }
4318            while self.check_preposition() || self.check_to() {
4319                // "within N cycles" is a temporal bound, not a PP — leave for try_wrap_bounded_delay
4320                if self.check_preposition_is("within") && self.current + 1 < self.tokens.len()
4321                    && matches!(self.tokens[self.current + 1].kind, TokenType::Cardinal(_) | TokenType::Number(_))
4322                {
4323                    break;
4324                }
4325                let prep_token = self.advance().clone();
4326                let prep_name = if let TokenType::Preposition(sym) = prep_token.kind {
4327                    sym
4328                } else if matches!(prep_token.kind, TokenType::To) {
4329                    self.interner.intern("To")
4330                } else {
4331                    continue;
4332                };
4333
4334                let pp_obj_term = if self.check(&TokenType::Reflexive) {
4335                    self.advance();
4336                    Term::Constant(subject_symbol)
4337                } else if self.check_pronoun() {
4338                    let token = self.advance().clone();
4339                    let (gender, number) = match &token.kind {
4340                        TokenType::Pronoun { gender, number, .. } => (*gender, *number),
4341                        TokenType::Ambiguous { primary, alternatives } => {
4342                            if let TokenType::Pronoun { gender, number, .. } = **primary {
4343                                (gender, number)
4344                            } else {
4345                                alternatives.iter().find_map(|t| {
4346                                    if let TokenType::Pronoun { gender, number, .. } = t {
4347                                        Some((*gender, *number))
4348                                    } else {
4349                                        None
4350                                    }
4351                                }).unwrap_or((Gender::Unknown, Number::Singular))
4352                            }
4353                        }
4354                        _ => (Gender::Unknown, Number::Singular),
4355                    };
4356                    let resolved = self.resolve_pronoun(gender, number)?;
4357                    match resolved {
4358                        super::ResolvedPronoun::Variable(s) => Term::Variable(s),
4359                        super::ResolvedPronoun::Constant(s) => Term::Constant(s),
4360                    }
4361                } else if self.check_content_word() || self.check_article() {
4362                    let prep_obj = self.parse_noun_phrase(false)?;
4363                    Term::Constant(prep_obj.noun)
4364                } else if self.check_number() {
4365                    // "N unit NOUN" is a measure-premodified noun ("brew with 190
4366                    // degree WATER") — ONE folded entity, not a measure with the
4367                    // head stranded. A noun/ambiguous head after the unit signals
4368                    // it; else the PP object is the bare measure ("sold for $105").
4369                    let premodified = matches!(
4370                        self.tokens.get(self.current + 2).map(|t| &t.kind),
4371                        Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
4372                    );
4373                    if premodified {
4374                        let saved_ctx = self.nominal_np_context;
4375                        self.nominal_np_context = true;
4376                        let r = self.parse_noun_phrase(false);
4377                        self.nominal_np_context = saved_ctx;
4378                        Term::Constant(r?.noun)
4379                    } else {
4380                        *self.parse_measure_phrase()?
4381                    }
4382                } else if gap_object {
4383                    // Preposition stranding: the object position was a wh-gap,
4384                    // so the bare preposition is licensed ("Who did John talk to?").
4385                    continue;
4386                } else if self.at_clause_boundary()
4387                    && crate::lexicon::is_particle(
4388                        &self.interner.resolve(prep_name).to_lowercase(),
4389                    )
4390                {
4391                    // A clause-final object-less PARTICLE preposition is an
4392                    // intransitive directional ("walked in", "sat down") — a
4393                    // lexically listed class; "of"/"to" cannot end a clause.
4394                    let event_sym = self.get_event_var();
4395                    pp_predicates.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
4396                        name: prep_name,
4397                        args: self.ctx.terms.alloc_slice([Term::Variable(event_sym)]),
4398                        world: None,
4399                    }));
4400                    continue;
4401                } else {
4402                    // A mid-clause preposition with no object is not a PP —
4403                    // hand it back so the sentence-level parse reports it
4404                    // instead of silently dropping it.
4405                    self.current -= 1;
4406                    break;
4407                };
4408
4409                if self.pp_attach_to_noun {
4410                    if let Some(obj) = object_term {
4411                        let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4412                            name: prep_name,
4413                            args: self.ctx.terms.alloc_slice([obj, pp_obj_term]),
4414                            world: None,
4415                        });
4416                        pp_predicates.push(pp_pred);
4417                    } else {
4418                        args.push(pp_obj_term);
4419                    }
4420                } else {
4421                    let event_sym = self.get_event_var();
4422                    let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4423                        name: prep_name,
4424                        args: self
4425                            .ctx
4426                            .terms
4427                            .alloc_slice([Term::Variable(event_sym), pp_obj_term]),
4428                        world: None,
4429                    });
4430                    pp_predicates.push(pp_pred);
4431                }
4432            }
4433
4434            if self.check(&TokenType::That) || self.check(&TokenType::Who) {
4435                self.advance();
4436                let rel_var = self.next_var_name();
4437                let rel_pred = self.parse_relative_clause(rel_var)?;
4438                pp_predicates.push(rel_pred);
4439            }
4440
4441            // "X has Y as [its/his/her] ROLE" — a predicative secondary on the
4442            // object: Y fills ROLE for the subject ("the city has Al Acosta as its
4443            // mayor" → Have(City, Al_Acosta) ∧ Mayor(Al_Acosta)). The Have-link
4444            // already binds Y to the subject, so the possessive determiner is
4445            // redundant and dropped. "as" lexes as a Noun (function-word fallback),
4446            // hence the lexeme test; the noun-compound loop leaves it unbundled.
4447            if object_term.is_some()
4448                && matches!(self.peek().kind, TokenType::Noun(_))
4449                && self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("as")
4450            {
4451                self.advance(); // consume "as"
4452                if self.check_possessive_pronoun() {
4453                    self.advance(); // consume "its" / "his" / "her"
4454                }
4455                if self.check_content_word() {
4456                    as_role = Some(self.consume_content_word()?);
4457                }
4458            }
4459
4460            let mut modifiers = self.collect_adverbs();
4461
4462            let effective_time = self.pending_time.take().unwrap_or(verb_time);
4463            match effective_time {
4464                Time::Past => modifiers.push(self.interner.intern("Past")),
4465                Time::Future => modifiers.push(self.interner.intern("Future")),
4466                _ => {}
4467            }
4468
4469            if verb_aspect == Aspect::Progressive {
4470                modifiers.push(self.interner.intern("Progressive"));
4471            } else if verb_aspect == Aspect::Perfect {
4472                modifiers.push(self.interner.intern("Perfect"));
4473            }
4474
4475            let mut roles: Vec<(ThematicRole, Term<'a>)> = Vec::new();
4476
4477            // Check if verb is unaccusative (intransitive subject is Theme, not Agent)
4478            let verb_str = self.interner.resolve(verb).to_lowercase();
4479            let is_unaccusative = crate::lexicon::lookup_verb_db(&verb_str)
4480                .map(|meta| meta.features.contains(&crate::lexicon::Feature::Unaccusative))
4481                .unwrap_or(false);
4482
4483            // Unaccusative verbs used intransitively: subject is Theme
4484            // E.g., "The alarm triggers" → Theme(e, Alarm), not Agent(e, Alarm)
4485            let has_object = object_term.is_some() || second_object_term.is_some();
4486            let subject_role = if is_unaccusative && !has_object {
4487                ThematicRole::Theme
4488            } else {
4489                ThematicRole::Agent
4490            };
4491
4492            roles.push((subject_role, subject_term));
4493            if let Some(second_obj) = second_object_term {
4494                if let Some(first_obj) = object_term {
4495                    roles.push((ThematicRole::Recipient, first_obj));
4496                }
4497                roles.push((ThematicRole::Theme, second_obj));
4498            } else if let Some(obj) = object_term {
4499                roles.push((ThematicRole::Theme, obj));
4500            }
4501
4502            let event_var = self.get_event_var();
4503            let suppress_existential = self.drs.in_conditional_antecedent();
4504            if suppress_existential {
4505                let event_class = self.interner.intern("Event");
4506                self.drs.introduce_referent(event_var, event_class, Gender::Neuter, Number::Singular);
4507            }
4508            let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
4509                event_var,
4510                verb,
4511                roles: self.ctx.roles.alloc_slice(roles.clone()),
4512                modifiers: self.ctx.syms.alloc_slice(modifiers.clone()),
4513                suppress_existential,
4514                world: None,
4515            })));
4516
4517            // Capture template for ellipsis reconstruction
4518            self.capture_event_template(verb, &roles, &modifiers);
4519
4520            let with_pps = if pp_predicates.is_empty() {
4521                neo_event
4522            } else {
4523                let mut combined = neo_event;
4524                for pp in pp_predicates {
4525                    combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4526                        left: combined,
4527                        op: TokenType::And,
4528                        right: pp,
4529                    });
4530                }
4531                combined
4532            };
4533
4534            // Include PPs attached to object NP (for NP-attachment mode)
4535            // These have _PP_SELF_ placeholder that needs to be replaced with the object term
4536            let with_object_pps = if object_pps.is_empty() {
4537                with_pps
4538            } else if let Some(obj_term) = object_term {
4539                let mut combined = with_pps;
4540                for pp in object_pps {
4541                    // Rebind the `_PP_SELF_` gap to the object term, recursing
4542                    // through connectives / quantifiers / events so a reduced
4543                    // relative restrictor ("the friend Simon went WITH" →
4544                    // ∃e(Go(e) ∧ Agent(e,Simon) ∧ With(e, _PP_SELF_))) binds its
4545                    // stranded-preposition gap to the head, not just a flat PP.
4546                    let substituted = self.substitute_pp_self_term(pp, obj_term);
4547                    combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4548                        left: combined,
4549                        op: TokenType::And,
4550                        right: substituted,
4551                    });
4552                }
4553                combined
4554            } else {
4555                with_pps
4556            };
4557
4558            // Predicate the definite/constant object's adjectives of it ("ate the
4559            // RED apple" → Red(Apple)); like the PPs above, dropping them loses a
4560            // constraint.
4561            let with_object_pps = if object_adjectives.is_empty() {
4562                with_object_pps
4563            } else if let Some(obj_term) = object_term {
4564                let mut combined = with_object_pps;
4565                for &adj in object_adjectives {
4566                    let adj_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4567                        name: adj,
4568                        args: self.ctx.terms.alloc_slice([obj_term]),
4569                        world: None,
4570                    });
4571                    combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4572                        left: combined,
4573                        op: TokenType::And,
4574                        right: adj_pred,
4575                    });
4576                }
4577                combined
4578            } else {
4579                with_object_pps
4580            };
4581
4582            // Apply aspectual operators based on verb class
4583            let with_aspect = if verb_aspect == Aspect::Simple && effective_time == Time::Present {
4584                // Non-state verbs in simple present get Habitual reading
4585                if !verb_class.is_stative() {
4586                    self.ctx.exprs.alloc(LogicExpr::Aspectual {
4587                        operator: AspectOperator::Habitual,
4588                        body: with_object_pps,
4589                    })
4590                } else {
4591                    with_object_pps
4592                }
4593            } else if verb_aspect == Aspect::Progressive {
4594                // Semelfactive + Progressive → Iterative
4595                if verb_class == crate::lexicon::VerbClass::Semelfactive {
4596                    self.ctx.exprs.alloc(LogicExpr::Aspectual {
4597                        operator: AspectOperator::Iterative,
4598                        body: with_object_pps,
4599                    })
4600                } else {
4601                    with_object_pps
4602                }
4603            } else {
4604                with_object_pps
4605            };
4606
4607            // Conjoin the predicative-secondary role ("as its mayor" → Mayor(Y)).
4608            let with_aspect = if let (Some(role), Some(obj)) = (as_role, object_term) {
4609                let role_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
4610                    name: role,
4611                    args: self.ctx.terms.alloc_slice([obj]),
4612                    world: None,
4613                });
4614                self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4615                    left: with_aspect,
4616                    op: TokenType::And,
4617                    right: role_pred,
4618                })
4619            } else {
4620                with_aspect
4621            };
4622
4623            Ok(with_aspect)
4624    }
4625}
4626
4627impl<'a, 'ctx, 'int> LogicVerbParsing<'a, 'ctx, 'int> for Parser<'a, 'ctx, 'int> {
4628    fn parse_predicate_with_subject(&mut self, subject_symbol: Symbol) -> ParseResult<&'a LogicExpr<'a>> {
4629        let result = self.parse_predicate_impl(subject_symbol, false)?;
4630        Ok(self.try_wrap_bounded_delay(result))
4631    }
4632
4633    fn parse_predicate_with_subject_as_var(&mut self, subject_symbol: Symbol) -> ParseResult<&'a LogicExpr<'a>> {
4634        let result = self.parse_predicate_impl(subject_symbol, true)?;
4635        Ok(self.try_wrap_bounded_delay(result))
4636    }
4637
4638    fn try_parse_plural_subject(
4639        &mut self,
4640        first_subject: &NounPhrase<'a>,
4641    ) -> Result<Option<&'a LogicExpr<'a>>, ParseError> {
4642        let saved_pos = self.current;
4643
4644        // Consume the 'and' we already peeked
4645        self.advance();
4646
4647        if !self.check_content_word() {
4648            self.current = saved_pos;
4649            return Ok(None);
4650        }
4651
4652        // Collect all subjects: "John and Mary and Sue"
4653        let mut subjects: Vec<Symbol> = vec![first_subject.noun];
4654
4655        loop {
4656            if !self.check_content_word() {
4657                break;
4658            }
4659            let next_subject = match self.parse_noun_phrase(true) {
4660                Ok(np) => np,
4661                Err(_) => {
4662                    self.current = saved_pos;
4663                    return Ok(None);
4664                }
4665            };
4666            subjects.push(next_subject.noun);
4667
4668            if self.check(&TokenType::And) {
4669                self.advance();
4670            } else {
4671                break;
4672            }
4673        }
4674
4675        // Check for copula (is/are/was/were) with predicate nominative
4676        // "Both Socrates and Plato are men" -> M(s) ∧ M(p)
4677        if self.check(&TokenType::Is) || self.check(&TokenType::Are)
4678            || self.check(&TokenType::Was) || self.check(&TokenType::Were)
4679        {
4680            let copula_time = if self.check(&TokenType::Was) || self.check(&TokenType::Were) {
4681                Time::Past
4682            } else {
4683                Time::Present
4684            };
4685            self.advance(); // consume the copula
4686
4687            // Check for negation: "are not valid", "are not both valid"
4688            let is_negated = self.check(&TokenType::Not);
4689            if is_negated {
4690                self.advance(); // consume "not"
4691            }
4692
4693            // Check for "both" modifier: "are not both valid"
4694            // "both" scopes negation over the conjunction: ¬(P(A) ∧ P(B))
4695            // Without "both": negation distributes: ¬P(A) ∧ ¬P(B)
4696            let has_both = self.check(&TokenType::Both);
4697            if has_both {
4698                self.advance(); // consume "both"
4699            }
4700
4701            // Parse the predicate (e.g., "men" in "are men", "valid" in "are valid")
4702            if !self.check_content_word() && !self.check_article() {
4703                self.current = saved_pos;
4704                return Ok(None);
4705            }
4706
4707            let predicate_np = match self.parse_noun_phrase(false) {
4708                Ok(np) => np,
4709                Err(_) => {
4710                    self.current = saved_pos;
4711                    return Ok(None);
4712                }
4713            };
4714            let predicate = predicate_np.noun;
4715
4716            // "A and B are DIFFERENT people" — the adjective "different" asserts
4717            // the members are pairwise DISTINCT (the puzzle solver's AllDifferent
4718            // constraint). Dropping it loses the constraint, so it is kept as
4719            // ¬(si = sj) below; mirrors the comma-list subject path.
4720            let is_different = predicate_np.adjectives.iter().any(|a| {
4721                self.interner.resolve(*a).eq_ignore_ascii_case("different")
4722            });
4723
4724            // A category DECLARATION ("Bill, Lillie, … are four different friends.",
4725            // "2001, … are four different years.") records item→category in the
4726            // shared discourse DRS, so a later definite LABEL ("the 2003 holiday",
4727            // "the Florida trip") can recover the category and un-fuse to the same
4728            // relation the prepositional-phrase form produces. The predicate
4729            // nominal is the category noun; each coordinated subject is an item of
4730            // that category.
4731            for subj in &subjects {
4732                self.drs.register_item_category(*subj, predicate);
4733            }
4734
4735            // Build distributed predicate: P(s1) ∧ P(s2) ∧ ...
4736            let mut conjuncts: Vec<&'a LogicExpr<'a>> = Vec::new();
4737            for subj in &subjects {
4738                let pred_expr = self.ctx.exprs.alloc(LogicExpr::Predicate {
4739                    name: predicate,
4740                    args: self.ctx.terms.alloc_slice([Term::Constant(*subj)]),
4741                    world: None,
4742                });
4743                conjuncts.push(pred_expr);
4744            }
4745            if is_different {
4746                for i in 0..subjects.len() {
4747                    for j in (i + 1)..subjects.len() {
4748                        let eq = self.ctx.exprs.alloc(LogicExpr::Identity {
4749                            left: self.ctx.terms.alloc(Term::Constant(subjects[i])),
4750                            right: self.ctx.terms.alloc(Term::Constant(subjects[j])),
4751                        });
4752                        conjuncts.push(self.ctx.exprs.alloc(LogicExpr::UnaryOp {
4753                            op: TokenType::Not,
4754                            operand: eq,
4755                        }));
4756                    }
4757                }
4758            }
4759
4760            if is_negated && !has_both {
4761                // "are not valid" → ¬P(s1) ∧ ¬P(s2) (negation distributes)
4762                for conjunct in &mut conjuncts {
4763                    *conjunct = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
4764                        op: TokenType::Not,
4765                        operand: *conjunct,
4766                    });
4767                }
4768            }
4769
4770            // Fold conjuncts into binary conjunction tree
4771            let mut result = conjuncts[0];
4772            for conjunct in &conjuncts[1..] {
4773                result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4774                    left: result,
4775                    op: TokenType::And,
4776                    right: *conjunct,
4777                });
4778            }
4779
4780            // "are not both valid" → ¬(P(s1) ∧ P(s2)) (negation over conjunction)
4781            if is_negated && has_both {
4782                result = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
4783                    op: TokenType::Not,
4784                    operand: result,
4785                });
4786            }
4787
4788            // Apply temporal modifier for past tense
4789            let with_time = match copula_time {
4790                Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
4791                    operator: TemporalOperator::Past,
4792                    body: result,
4793                }),
4794                _ => result,
4795            };
4796
4797            return Ok(Some(with_time));
4798        }
4799
4800        if !self.check_verb() {
4801            self.current = saved_pos;
4802            return Ok(None);
4803        }
4804
4805        // Coordinated subjects registered in DRS via introduce_referent
4806
4807        let (verb, verb_time, _verb_aspect, _) = self.consume_verb_with_metadata();
4808
4809        // Check for reciprocal: "John and Mary kicked each other"
4810        if self.check(&TokenType::Reciprocal) {
4811            self.advance();
4812            if subjects.len() != 2 {
4813                self.current = saved_pos;
4814                return Ok(None);
4815            }
4816            let pred1 = self.ctx.exprs.alloc(LogicExpr::Predicate {
4817                name: verb,
4818                args: self.ctx.terms.alloc_slice([
4819                    Term::Constant(subjects[0]),
4820                    Term::Constant(subjects[1]),
4821                ]),
4822                world: None,
4823            });
4824            let pred2 = self.ctx.exprs.alloc(LogicExpr::Predicate {
4825                name: verb,
4826                args: self.ctx.terms.alloc_slice([
4827                    Term::Constant(subjects[1]),
4828                    Term::Constant(subjects[0]),
4829                ]),
4830                world: None,
4831            });
4832            let expr = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4833                left: pred1,
4834                op: TokenType::And,
4835                right: pred2,
4836            });
4837
4838            let with_time = match verb_time {
4839                Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
4840                    operator: TemporalOperator::Past,
4841                    body: expr,
4842                }),
4843                Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
4844                    operator: TemporalOperator::Future,
4845                    body: expr,
4846                }),
4847                _ => expr,
4848            };
4849            return Ok(Some(with_time));
4850        }
4851
4852        // Check for objects (for transitive verbs with "respectively")
4853        let mut objects: Vec<Symbol> = Vec::new();
4854        if self.check_content_word() || self.check_article() {
4855            // Parse first object
4856            let first_obj = match self.parse_noun_phrase(false) {
4857                Ok(np) => np,
4858                Err(_) => {
4859                    // No objects, continue with intransitive
4860                    return Ok(Some(self.build_group_predicate(&subjects, verb, verb_time)));
4861                }
4862            };
4863            objects.push(first_obj.noun);
4864
4865            // Parse additional objects: "Tom and Jerry and Bob"
4866            while self.check(&TokenType::And) {
4867                self.advance();
4868                if self.check_content_word() || self.check_article() {
4869                    let next_obj = match self.parse_noun_phrase(false) {
4870                        Ok(np) => np,
4871                        Err(_) => break,
4872                    };
4873                    objects.push(next_obj.noun);
4874                } else {
4875                    break;
4876                }
4877            }
4878        }
4879
4880        // Check for "respectively" - triggers pairwise interpretation
4881        // Ditransitive pairing ("gave books TO TOM AND JERRY respectively"):
4882        // the recipients, not the shared theme, line up with the subjects.
4883        let mut recipients: Vec<Symbol> = Vec::new();
4884        let respectively_ahead = {
4885            let mut i = self.current;
4886            let mut found = false;
4887            while i < self.tokens.len()
4888                && !matches!(self.tokens[i].kind, TokenType::Period | TokenType::EOF)
4889            {
4890                if matches!(self.tokens[i].kind, TokenType::Respectively) {
4891                    found = true;
4892                    break;
4893                }
4894                i += 1;
4895            }
4896            found
4897        };
4898        if respectively_ahead && self.check_to_marker() {
4899            self.advance(); // to
4900            loop {
4901                let r_np = self.parse_noun_phrase(false)?;
4902                recipients.push(r_np.noun);
4903                if self.check(&TokenType::And) {
4904                    self.advance();
4905                } else {
4906                    break;
4907                }
4908            }
4909        }
4910
4911        if self.check(&TokenType::Respectively) {
4912            let respectively_span = self.peek().span;
4913            self.advance(); // consume "respectively"
4914
4915            let pair_targets: &[Symbol] = if recipients.is_empty() {
4916                &objects
4917            } else {
4918                &recipients
4919            };
4920            if subjects.len() != pair_targets.len() {
4921                return Err(ParseError {
4922                    kind: ParseErrorKind::RespectivelyLengthMismatch {
4923                        subject_count: subjects.len(),
4924                        object_count: pair_targets.len(),
4925                    },
4926                    span: respectively_span,
4927                });
4928            }
4929
4930            // Build pairwise predicates: See(J,T) ∧ See(M,J) ∧ ...; with
4931            // recipients, the theme is shared: Give(J,Books,T) ∧ Give(M,Books,J).
4932            let mut conjuncts: Vec<&'a LogicExpr<'a>> = Vec::new();
4933            let suppress_existential = self.drs.in_conditional_antecedent();
4934            for (subj, target) in subjects.iter().zip(pair_targets.iter()) {
4935                let event_var = self.get_event_var();
4936                let roles = if recipients.is_empty() {
4937                    vec![
4938                        (ThematicRole::Agent, Term::Constant(*subj)),
4939                        (ThematicRole::Theme, Term::Constant(*target)),
4940                    ]
4941                } else {
4942                    let mut r = vec![(ThematicRole::Agent, Term::Constant(*subj))];
4943                    if let Some(theme) = objects.first() {
4944                        r.push((ThematicRole::Theme, Term::Constant(*theme)));
4945                    }
4946                    r.push((ThematicRole::Recipient, Term::Constant(*target)));
4947                    r
4948                };
4949                let neo_event = self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
4950                    event_var,
4951                    verb,
4952                    roles: self.ctx.roles.alloc_slice(roles),
4953                    modifiers: self.ctx.syms.alloc_slice(vec![]),
4954                    suppress_existential,
4955                    world: None,
4956                })));
4957                conjuncts.push(neo_event);
4958            }
4959
4960            // Fold conjuncts into binary conjunction tree
4961            let mut result = conjuncts[0];
4962            for conjunct in &conjuncts[1..] {
4963                result = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
4964                    left: result,
4965                    op: TokenType::And,
4966                    right: *conjunct,
4967                });
4968            }
4969
4970            // Apply temporal modifier
4971            let with_time = match verb_time {
4972                Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
4973                    operator: TemporalOperator::Past,
4974                    body: result,
4975                }),
4976                Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
4977                    operator: TemporalOperator::Future,
4978                    body: result,
4979                }),
4980                _ => result,
4981            };
4982
4983            return Ok(Some(with_time));
4984        }
4985
4986        // No "respectively" - use group semantics
4987        if objects.is_empty() {
4988            // Intransitive: group subject
4989            Ok(Some(self.build_group_predicate(&subjects, verb, verb_time)))
4990        } else {
4991            // Transitive without "respectively": group subject, group object
4992            Ok(Some(self.build_group_transitive(&subjects, &objects, verb, verb_time)))
4993        }
4994    }
4995
4996    /// Build a group predicate for intransitive verbs
4997    fn build_group_predicate(
4998        &mut self,
4999        subjects: &[Symbol],
5000        verb: Symbol,
5001        verb_time: Time,
5002    ) -> &'a LogicExpr<'a> {
5003        let group_members: Vec<Term<'a>> = subjects.iter()
5004            .map(|s| Term::Constant(*s))
5005            .collect();
5006        let group_members_slice = self.ctx.terms.alloc_slice(group_members);
5007
5008        let expr = self.ctx.exprs.alloc(LogicExpr::Predicate {
5009            name: verb,
5010            args: self.ctx.terms.alloc_slice([Term::Group(group_members_slice)]),
5011            world: None,
5012        });
5013
5014        match verb_time {
5015            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
5016                operator: TemporalOperator::Past,
5017                body: expr,
5018            }),
5019            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
5020                operator: TemporalOperator::Future,
5021                body: expr,
5022            }),
5023            _ => expr,
5024        }
5025    }
5026
5027    /// Build a transitive predicate with group subject and group object
5028    fn build_group_transitive(
5029        &mut self,
5030        subjects: &[Symbol],
5031        objects: &[Symbol],
5032        verb: Symbol,
5033        verb_time: Time,
5034    ) -> &'a LogicExpr<'a> {
5035        let subj_members: Vec<Term<'a>> = subjects.iter()
5036            .map(|s| Term::Constant(*s))
5037            .collect();
5038        let obj_members: Vec<Term<'a>> = objects.iter()
5039            .map(|o| Term::Constant(*o))
5040            .collect();
5041
5042        let subj_group = Term::Group(self.ctx.terms.alloc_slice(subj_members));
5043        let obj_group = Term::Group(self.ctx.terms.alloc_slice(obj_members));
5044
5045        let expr = self.ctx.exprs.alloc(LogicExpr::Predicate {
5046            name: verb,
5047            args: self.ctx.terms.alloc_slice([subj_group, obj_group]),
5048            world: None,
5049        });
5050
5051        match verb_time {
5052            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
5053                operator: TemporalOperator::Past,
5054                body: expr,
5055            }),
5056            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
5057                operator: TemporalOperator::Future,
5058                body: expr,
5059            }),
5060            _ => expr,
5061        }
5062    }
5063
5064    fn parse_control_structure(
5065        &mut self,
5066        subject: &NounPhrase<'a>,
5067        verb: Symbol,
5068        verb_time: Time,
5069    ) -> ParseResult<&'a LogicExpr<'a>> {
5070        let subject_sym = subject.noun;
5071        let verb_str = self.interner.resolve(verb);
5072
5073        if Lexer::is_raising_verb(verb_str) {
5074            if !self.check_to() {
5075                return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
5076                    name: verb,
5077                    args: self.ctx.terms.alloc_slice([Term::Constant(subject_sym)]),
5078                    world: None,
5079                }));
5080            }
5081            self.advance();
5082
5083            if !self.check_verb() {
5084                return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
5085                    name: verb,
5086                    args: self.ctx.terms.alloc_slice([Term::Constant(subject_sym)]),
5087                    world: None,
5088                }));
5089            }
5090
5091            let inf_verb = self.consume_verb();
5092
5093            let embedded = if self.is_control_verb(inf_verb) {
5094                let raised_np = NounPhrase {
5095                    noun: subject_sym,
5096                    definiteness: None,
5097                    adjectives: &[],
5098                    possessor: None,
5099                    pps: &[],
5100                    superlative: None,
5101                };
5102                self.parse_control_structure(&raised_np, inf_verb, Time::None)?
5103            } else {
5104                self.ctx.exprs.alloc(LogicExpr::Predicate {
5105                    name: inf_verb,
5106                    args: self.ctx.terms.alloc_slice([Term::Constant(subject_sym)]),
5107                    world: None,
5108                })
5109            };
5110
5111            let result = self.ctx.exprs.alloc(LogicExpr::Scopal {
5112                operator: verb,
5113                body: embedded,
5114            });
5115
5116            return Ok(match verb_time {
5117                Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
5118                    operator: TemporalOperator::Past,
5119                    body: result,
5120                }),
5121                Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
5122                    operator: TemporalOperator::Future,
5123                    body: result,
5124                }),
5125                _ => result,
5126            });
5127        }
5128
5129        let is_object_control = Lexer::is_object_control_verb(self.interner.resolve(verb));
5130        let (object_term, pro_controller_sym) = if self.check_to() {
5131            (None, subject_sym)
5132        } else if self.check_content_word() {
5133            let object_np = self.parse_noun_phrase(false)?;
5134            let obj_sym = object_np.noun;
5135
5136            let controller = if is_object_control {
5137                obj_sym
5138            } else {
5139                subject_sym
5140            };
5141            (
5142                Some(self.ctx.terms.alloc(Term::Constant(obj_sym))),
5143                controller,
5144            )
5145        } else {
5146            (None, subject_sym)
5147        };
5148
5149        if !self.check_to() {
5150            return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
5151                name: verb,
5152                args: match object_term {
5153                    Some(obj) => self.ctx.terms.alloc_slice([
5154                        Term::Constant(subject_sym),
5155                        Term::Constant(match obj {
5156                            Term::Constant(s) => *s,
5157                            _ => subject_sym,
5158                        }),
5159                    ]),
5160                    None => self.ctx.terms.alloc_slice([Term::Constant(subject_sym)]),
5161                },
5162                world: None,
5163            }));
5164        }
5165        self.advance();
5166
5167        if !self.check_verb() {
5168            return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
5169                name: verb,
5170                args: self.ctx.terms.alloc_slice([Term::Constant(subject_sym)]),
5171                world: None,
5172            }));
5173        }
5174
5175        let inf_verb = self.consume_verb();
5176        let inf_verb_str = self.interner.resolve(inf_verb).to_lowercase();
5177
5178        let infinitive = if inf_verb_str == "be" && self.check_verb() {
5179            let passive_verb = self.consume_verb();
5180            // An agent by-phrase fills the first argument slot, matching the
5181            // finite passive ("was seen by the people" → See(People, s)).
5182            let mut passive_args = vec![Term::Constant(pro_controller_sym)];
5183            // A DESCRIPTIVE control-passive by-agent ("…to be fed by the old man")
5184            // becomes its own restrictor-carrying entity scoping the relation; a
5185            // bare one keeps the constant form.
5186            let mut agent_restr: Option<(Symbol, &'a LogicExpr<'a>)> = None;
5187            if self.check_preposition_is("by")
5188                && self
5189                    .tokens
5190                    .get(self.current + 1)
5191                    .map_or(false, |t| matches!(
5192                        t.kind,
5193                        TokenType::ProperName(_) | TokenType::Noun(_) | TokenType::Article(_)
5194                    ))
5195            {
5196                self.advance(); // by
5197                let agent_np = self.parse_noun_phrase(false)?;
5198                let (agent_term, restr) = self.possessor_entity(&agent_np);
5199                agent_restr = restr;
5200                passive_args.insert(0, agent_term);
5201            }
5202            let passive_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
5203                name: passive_verb,
5204                args: self.ctx.terms.alloc_slice(passive_args),
5205                world: None,
5206            });
5207            let passive_pred = self.wrap_in_possessor_entity(agent_restr, passive_pred);
5208            self.ctx.voice(crate::ast::VoiceOperator::Passive, passive_pred)
5209        } else if self.is_control_verb(inf_verb) {
5210            let controller_np = NounPhrase {
5211                noun: pro_controller_sym,
5212                definiteness: None,
5213                adjectives: &[],
5214                possessor: None,
5215                pps: &[],
5216                superlative: None,
5217            };
5218            self.parse_control_structure(&controller_np, inf_verb, Time::None)?
5219        } else {
5220            self.ctx.exprs.alloc(LogicExpr::Predicate {
5221                name: inf_verb,
5222                args: self
5223                    .ctx
5224                    .terms
5225                    .alloc_slice([Term::Constant(pro_controller_sym)]),
5226                world: None,
5227            })
5228        };
5229
5230        let control = self.ctx.exprs.alloc(LogicExpr::Control {
5231            verb,
5232            subject: self.ctx.terms.alloc(Term::Constant(subject_sym)),
5233            object: object_term,
5234            infinitive,
5235        });
5236
5237        Ok(match verb_time {
5238            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
5239                operator: TemporalOperator::Past,
5240                body: control,
5241            }),
5242            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
5243                operator: TemporalOperator::Future,
5244                body: control,
5245            }),
5246            _ => control,
5247        })
5248    }
5249
5250    fn is_control_verb(&self, verb: Symbol) -> bool {
5251        let lemma = self.interner.resolve(verb);
5252        Lexer::is_subject_control_verb(lemma)
5253            || Lexer::is_object_control_verb(lemma)
5254            || Lexer::is_raising_verb(lemma)
5255    }
5256}