Skip to main content

logicaffeine_language/parser/
pragmatics.rs

1//! Pragmatic inference and focus-sensitive parsing.
2//!
3//! This module handles linguistic phenomena that go beyond pure syntax/semantics:
4//!
5//! - **Focus particles**: "only", "even", "also" with alternative semantics
6//! - **Presupposition triggers**: "stop", "continue", "too"
7//! - **Measure phrases**: Dimensional expressions with units
8//! - **Comparatives**: "taller than", "as tall as", degree semantics
9//! - **Superlatives**: "the tallest", unique maximal individuals
10//! - **Scopal adverbs**: "always", "never", "usually"
11//!
12//! Focus is represented using the `LogicExpr::Focus` variant with an alternatives
13//! set derived from the focus domain.
14
15use super::clause::ClauseParsing;
16use super::noun::NounParsing;
17use super::quantifier::QuantifierParsing;
18use super::{ParseResult, Parser};
19use crate::ast::{LogicExpr, NounPhrase, NumberKind, QuantifierKind, TemporalOperator, Term};
20use crate::error::{ParseError, ParseErrorKind};
21use crate::lexicon::{self, Time};
22use crate::token::{MeasureKind, PresupKind, TokenType};
23
24/// Trait for parsing pragmatic and focus-sensitive constructions.
25///
26/// Provides methods for parsing linguistic phenomena that go beyond pure
27/// syntax/semantics, including focus particles, presupposition triggers,
28/// measure phrases, degree expressions (comparatives/superlatives), and
29/// scopal adverbs.
30///
31/// Focus is represented using alternative semantics, where the focused
32/// element evokes a set of alternatives. Presuppositions project through
33/// various operators and represent background entailments.
34pub trait PragmaticsParsing<'a, 'ctx, 'int> {
35    /// Parses a focus particle construction: "only John runs", "even Mary left".
36    ///
37    /// Focus particles like "only", "even", and "also" introduce alternative
38    /// semantics. The focused element is contrasted with a contextually
39    /// determined set of alternatives.
40    ///
41    /// Returns a [`LogicExpr::Focus`] with the focus kind, focused element,
42    /// and the scope predicate.
43    fn parse_focus(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
44
45    /// Parses a measure construction: "much water is cold", "little food arrived".
46    ///
47    /// Measure expressions quantify over amounts using "much", "little", etc.
48    /// The result is an existentially quantified formula binding the measured
49    /// entity with a measure predicate.
50    fn parse_measure(&mut self) -> ParseResult<&'a LogicExpr<'a>>;
51
52    /// Parses a presupposition-triggering verb: "stopped running", "regrets leaving".
53    ///
54    /// Presupposition triggers introduce background entailments that project
55    /// through negation and other operators:
56    ///
57    /// - "stop P" presupposes: previously P; asserts: now ¬P
58    /// - "start P" presupposes: previously ¬P; asserts: now P
59    /// - "regret P" presupposes: P happened; asserts: subject regrets it
60    /// - "continue P" presupposes: was P; asserts: still P
61    ///
62    /// Returns a [`LogicExpr::Presupposition`] separating assertion from presupposition.
63    fn parse_presupposition(
64        &mut self,
65        subject: &NounPhrase<'a>,
66        presup_kind: PresupKind,
67        negated: bool,
68    ) -> ParseResult<&'a LogicExpr<'a>>;
69
70    /// Term-parametric form of [`Self::parse_presupposition`] — the subject is a
71    /// TERM (constant or a relativized variable) so the same presupposition
72    /// grammar composes over a relative-clause subject ("the person who won
73    /// started skydiving 2 years after …").
74    fn parse_presupposition_for_term(
75        &mut self,
76        subject_term: Term<'a>,
77        presup_kind: PresupKind,
78        negated: bool,
79    ) -> ParseResult<&'a LogicExpr<'a>>;
80
81    /// Parses a simple predicate for a given subject noun phrase.
82    ///
83    /// Handles verb phrases with optional objects and focus-marked objects
84    /// like "eats only rice". Used as a helper for focus and other pragmatic
85    /// constructions.
86    fn parse_predicate_for_subject(&mut self, subject: &NounPhrase<'a>)
87        -> ParseResult<&'a LogicExpr<'a>>;
88
89    /// Parses a scopal adverb construction: "always runs", "never sleeps".
90    ///
91    /// Scopal adverbs like "always", "never", "usually", "sometimes" quantify
92    /// over times, events, or situations. They create a [`LogicExpr::Scopal`]
93    /// operator that scopes over the verb predicate.
94    fn parse_scopal_adverb(&mut self, subject: &NounPhrase<'a>) -> ParseResult<&'a LogicExpr<'a>>;
95
96    /// Parses a superlative construction: "is the tallest student".
97    ///
98    /// Superlatives identify the unique maximal individual along a gradable
99    /// dimension within a comparison class. Returns a [`LogicExpr::Superlative`]
100    /// with the adjective, subject, and domain restrictor.
101    fn parse_superlative(&mut self, subject: &NounPhrase<'a>) -> ParseResult<&'a LogicExpr<'a>>;
102
103    /// Parses a comparative construction: "is taller than Mary", "is greater than 0".
104    ///
105    /// Comparatives establish an ordering relation along a gradable dimension.
106    /// Supports both NP comparisons ("taller than Mary") and numeric comparisons
107    /// ("greater than 0"). The optional `difference` parameter handles differential
108    /// comparatives like "3 inches taller".
109    ///
110    /// Returns a [`LogicExpr::Comparative`] with adjective, subject, and object.
111    fn parse_comparative(
112        &mut self,
113        subject: &NounPhrase<'a>,
114        copula_time: Time,
115        difference: Option<&'a Term<'a>>,
116    ) -> ParseResult<&'a LogicExpr<'a>>;
117
118    /// Parses the equative "X is as ADJ as Y" → an at-least (`≥`) comparison.
119    fn parse_equative(&mut self, subject: &NounPhrase<'a>) -> ParseResult<&'a LogicExpr<'a>>;
120
121    /// Checks if the current token is a numeric literal.
122    ///
123    /// Used to distinguish numeric comparisons ("greater than 0") from
124    /// entity comparisons ("taller than John").
125    fn check_number(&self) -> bool;
126
127    /// Parses a measure phrase: "5 meters", "100 kilograms".
128    ///
129    /// Measure phrases combine a numeric value with an optional unit.
130    /// The unit is looked up in the lexicon to determine its dimension
131    /// (length, mass, time, etc.).
132    ///
133    /// Returns a [`Term::Value`] with the parsed number, unit, and dimension.
134    fn parse_measure_phrase(&mut self) -> ParseResult<&'a Term<'a>>;
135
136    /// Recognises a digit-led COUNTING noun phrase in object position —
137    /// `Number (adjective)+ Noun` ("6 brown manatees", "49 previous jumps") —
138    /// and returns its integer count.
139    ///
140    /// The discriminator is the intervening adjective. A measure phrase is
141    /// `Number Unit` with no adjective ("190 points", "385 degrees"), so a bare
142    /// `Number Noun` stays a measure (the count and noun are preserved in the
143    /// [`Term::Value`] — no meaning loss, no regression). Only when an adjective
144    /// sits between the number and the head noun is the phrase unambiguously a
145    /// counting NP, which the caller routes through the canonical
146    /// cardinal-quantified-object machinery as `∃=n y(Noun(y) ∧ Adj(y) ∧ …)`
147    /// rather than mis-reading the adjective as a measure unit.
148    fn counting_np_lookahead(&self) -> Option<u32>;
149}
150
151impl<'a, 'ctx, 'int> PragmaticsParsing<'a, 'ctx, 'int> for Parser<'a, 'ctx, 'int> {
152    fn parse_focus(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
153        let kind = if let TokenType::Focus(k) = self.advance().kind {
154            k
155        } else {
156            return Err(ParseError {
157                kind: ParseErrorKind::ExpectedFocusParticle,
158                span: self.current_span(),
159            });
160        };
161
162        if self.check_quantifier() {
163            self.advance();
164            let quantified = self.parse_quantified()?;
165            let focus_var = self.interner.intern("focus");
166            let focused = self.ctx.terms.alloc(Term::Variable(focus_var));
167            return Ok(self.ctx.exprs.alloc(LogicExpr::Focus {
168                kind,
169                focused,
170                scope: quantified,
171            }));
172        }
173
174        let focused_np = self.parse_noun_phrase(true)?;
175        let focused = self.ctx.terms.alloc(Term::Constant(focused_np.noun));
176
177        let scope = self.parse_predicate_for_subject(&focused_np)?;
178
179        Ok(self.ctx.exprs.alloc(LogicExpr::Focus {
180            kind,
181            focused,
182            scope,
183        }))
184    }
185
186    fn parse_measure(&mut self) -> ParseResult<&'a LogicExpr<'a>> {
187        let kind = if let TokenType::Measure(k) = self.advance().kind {
188            k
189        } else {
190            return Err(ParseError {
191                kind: ParseErrorKind::UnexpectedToken {
192                    expected: TokenType::Measure(MeasureKind::Much),
193                    found: self.peek().kind.clone(),
194                },
195                span: self.current_span(),
196            });
197        };
198
199        let np = self.parse_noun_phrase(true)?;
200        let var = self.next_var_name();
201
202        let noun_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
203            name: np.noun,
204            args: self.ctx.terms.alloc_slice([Term::Variable(var)]),
205            world: None,
206        });
207
208        let measure_sym = self.interner.intern("Measure");
209        let kind_sym = self.interner.intern(match kind {
210            MeasureKind::Much => "Much",
211            MeasureKind::Little => "Little",
212        });
213        let measure_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
214            name: measure_sym,
215            args: self
216                .ctx
217                .terms
218                .alloc_slice([Term::Variable(var), Term::Constant(kind_sym)]),
219            world: None,
220        });
221
222        let (pred_expr, verb_time) = if self.check(&TokenType::Is) {
223            let copula_time = if let TokenType::Is = self.advance().kind {
224                Time::Present
225            } else {
226                Time::Present
227            };
228
229            // Check for comparative: "is colder than"
230            if self.check_comparative() {
231                let subj_np = NounPhrase {
232                    noun: np.noun,
233                    definiteness: None,
234                    adjectives: &[],
235                    possessor: None,
236                    pps: &[],
237                    superlative: None,
238                };
239                let comp_expr = self.parse_comparative(&subj_np, copula_time, None)?;
240
241                let combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
242                    left: noun_pred,
243                    op: TokenType::And,
244                    right: self.ctx.exprs.alloc(LogicExpr::BinaryOp {
245                        left: measure_pred,
246                        op: TokenType::And,
247                        right: comp_expr,
248                    }),
249                });
250
251                return Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
252                    kind: QuantifierKind::Existential,
253                    variable: var,
254                    body: combined,
255                    island_id: self.current_island,
256                }));
257            }
258
259            let adj = self.consume_content_word()?;
260            let adj_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
261                name: adj,
262                args: self.ctx.terms.alloc_slice([Term::Variable(var)]),
263                world: None,
264            });
265            (adj_pred, copula_time)
266        } else {
267            let (verb, verb_time, _, _) = self.consume_verb_with_metadata();
268            let verb_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
269                name: verb,
270                args: self.ctx.terms.alloc_slice([Term::Variable(var)]),
271                world: None,
272            });
273            (verb_pred, verb_time)
274        };
275
276        let combined = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
277            left: noun_pred,
278            op: TokenType::And,
279            right: self.ctx.exprs.alloc(LogicExpr::BinaryOp {
280                left: measure_pred,
281                op: TokenType::And,
282                right: pred_expr,
283            }),
284        });
285
286        let with_time = match verb_time {
287            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
288                operator: TemporalOperator::Past,
289                body: combined,
290            }),
291            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
292                operator: TemporalOperator::Future,
293                body: combined,
294            }),
295            _ => combined,
296        };
297
298        Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
299            kind: QuantifierKind::Existential,
300            variable: var,
301            body: with_time,
302            island_id: self.current_island,
303        }))
304    }
305
306    fn parse_presupposition(
307        &mut self,
308        subject: &NounPhrase<'a>,
309        presup_kind: PresupKind,
310        negated: bool,
311    ) -> ParseResult<&'a LogicExpr<'a>> {
312        // Delegate to the term-parametric form so the SAME presupposition grammar
313        // ("started skydiving 2 years after …") composes over a relativized
314        // variable too ("the person WHO WON started skydiving …"), not only a
315        // constant subject — LIFT AND SHIFT instead of duplicating the dispatch.
316        self.parse_presupposition_for_term(Term::Constant(subject.noun), presup_kind, negated)
317    }
318
319    fn parse_presupposition_for_term(
320        &mut self,
321        subject_term: Term<'a>,
322        presup_kind: PresupKind,
323        negated: bool,
324    ) -> ParseResult<&'a LogicExpr<'a>> {
325
326        let unknown = self.interner.intern("?");
327        let complement_verb = if self.check_verb() {
328            Some(self.consume_verb())
329        } else {
330            None
331        };
332        let complement = match complement_verb {
333            Some(verb) => self.ctx.exprs.alloc(LogicExpr::Predicate {
334                name: verb,
335                args: self.ctx.terms.alloc_slice([subject_term]),
336                world: None,
337            }),
338            None => self.ctx.exprs.alloc(LogicExpr::Atom(unknown)),
339        };
340        // The presupposed clause is a real past EVENT — the same shape the
341        // standalone sentence parses to ("Mary lied." →
342        // ∃e(Lie(e) ∧ Agent(e, Mary) ∧ Past(e))) — so the projected content
343        // is derivable by the proof engine, not just printable.
344        let past_event = match complement_verb {
345            Some(verb) => {
346                use crate::ast::logic::NeoEventData;
347                use crate::ast::ThematicRole;
348                self.ctx.exprs.alloc(LogicExpr::NeoEvent(Box::new(NeoEventData {
349                    event_var: self.interner.intern("e"),
350                    verb,
351                    roles: self
352                        .ctx
353                        .roles
354                        .alloc_slice(vec![(ThematicRole::Agent, subject_term)]),
355                    modifiers: self.ctx.syms.alloc_slice(vec![self.interner.intern("Past")]),
356                    suppress_existential: false,
357                    world: None,
358                })))
359            }
360            None => self.ctx.exprs.alloc(LogicExpr::Temporal {
361                operator: TemporalOperator::Past,
362                body: complement,
363            }),
364        };
365
366        let (mut assertion, presupposition) = match presup_kind {
367            PresupKind::Stop => {
368                let neg = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
369                    op: TokenType::Not,
370                    operand: complement,
371                });
372                (neg, past_event)
373            }
374            PresupKind::Start => {
375                let neg_past = self.ctx.exprs.alloc(LogicExpr::UnaryOp {
376                    op: TokenType::Not,
377                    operand: past_event,
378                });
379                (complement, neg_past)
380            }
381            PresupKind::Regret => {
382                let regret_sym = self.interner.intern("Regret");
383                let regret = self.ctx.exprs.alloc(LogicExpr::Predicate {
384                    name: regret_sym,
385                    args: self.ctx.terms.alloc_slice([subject_term]),
386                    world: None,
387                });
388                (regret, past_event)
389            }
390            PresupKind::Continue | PresupKind::Realize | PresupKind::Know => {
391                let verb_name = match presup_kind {
392                    PresupKind::Continue => self.interner.intern("Continue"),
393                    PresupKind::Realize => self.interner.intern("Realize"),
394                    PresupKind::Know => self.interner.intern("Know"),
395                    _ => unknown,
396                };
397                let main = self.ctx.exprs.alloc(LogicExpr::Predicate {
398                    name: verb_name,
399                    args: self.ctx.terms.alloc_slice([subject_term]),
400                    world: None,
401                });
402                (main, complement)
403            }
404        };
405
406        // Trailing temporal-offset adjunct on the complement event ("started
407        // skydiving 2 YEARS AFTER Leslie", "started skydiving sometime BEFORE
408        // Faye") attaches to the asserted clause; without this it strands.
409        let subj_term = subject_term;
410        if let Some(off) = self.parse_temporal_offset_constraint(subj_term)? {
411            assertion = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
412                left: assertion,
413                op: TokenType::And,
414                right: off,
415            });
416        } else if let Some(off) = self.parse_bare_temporal_constraint(subj_term)? {
417            assertion = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
418                left: assertion,
419                op: TokenType::And,
420                right: off,
421            });
422        }
423
424        // Van der Sandt projection: under negation the assertion is negated but the
425        // PRESUPPOSITION projects (survives outside the ¬). "Mary doesn't regret
426        // lying." → ¬Regret(Mary) [Presup: P(Lie(Mary))] — she still lied.
427        let assertion = if negated {
428            self.ctx.exprs.alloc(LogicExpr::UnaryOp {
429                op: TokenType::Not,
430                operand: assertion,
431            })
432        } else {
433            assertion
434        };
435
436        Ok(self.ctx.exprs.alloc(LogicExpr::Presupposition {
437            assertion,
438            presupposition,
439        }))
440    }
441
442    fn parse_predicate_for_subject(
443        &mut self,
444        subject: &NounPhrase<'a>,
445    ) -> ParseResult<&'a LogicExpr<'a>> {
446        if self.check_verb() {
447            let verb = self.consume_verb();
448
449            // Check for focused object: "eats only rice"
450            if self.check_focus() {
451                let focus_kind = if let TokenType::Focus(k) = self.advance().kind {
452                    k
453                } else {
454                    crate::token::FocusKind::Only
455                };
456
457                let object_np = self.parse_noun_phrase(false)?;
458                let object_term = Term::Constant(object_np.noun);
459
460                let predicate = self.ctx.exprs.alloc(LogicExpr::Predicate {
461                    name: verb,
462                    args: self.ctx.terms.alloc_slice([
463                        Term::Constant(subject.noun),
464                        object_term.clone(),
465                    ]),
466                    world: None,
467                });
468
469                return Ok(self.ctx.exprs.alloc(LogicExpr::Focus {
470                    kind: focus_kind,
471                    focused: self.ctx.terms.alloc(object_term),
472                    scope: predicate,
473                }));
474            }
475
476            let mut args = vec![Term::Constant(subject.noun)];
477
478            if self.check_content_word() || self.check_article() {
479                let object = self.parse_noun_phrase(false)?;
480                args.push(Term::Constant(object.noun));
481            }
482
483            Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
484                name: verb,
485                args: self.ctx.terms.alloc_slice(args),
486                world: None,
487            }))
488        } else if matches!(self.peek().kind, TokenType::Is | TokenType::Are) {
489            // Copular predication under focus: "Only dogs are red." →
490            // Only(D, Red(Dogs)), mirroring the verbal "Only dogs bark."
491            self.advance();
492            if self.check_article() {
493                self.advance();
494            }
495            let predicate = match self.advance().kind.clone() {
496                TokenType::Adjective(sym)
497                | TokenType::Noun(sym)
498                | TokenType::ProperName(sym) => sym,
499                TokenType::Ambiguous { primary, alternatives } => {
500                    let as_predicate = |t: &TokenType| match t {
501                        TokenType::Adjective(sym)
502                        | TokenType::Noun(sym)
503                        | TokenType::ProperName(sym) => Some(*sym),
504                        _ => None,
505                    };
506                    match as_predicate(&primary)
507                        .or_else(|| alternatives.iter().find_map(as_predicate))
508                    {
509                        Some(sym) => sym,
510                        None => {
511                            return Err(ParseError {
512                                kind: ParseErrorKind::ExpectedContentWord {
513                                    found: *primary,
514                                },
515                                span: self.current_span(),
516                            });
517                        }
518                    }
519                }
520                found => {
521                    return Err(ParseError {
522                        kind: ParseErrorKind::ExpectedContentWord { found },
523                        span: self.current_span(),
524                    });
525                }
526            };
527            Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
528                name: predicate,
529                args: self.ctx.terms.alloc_slice([Term::Constant(subject.noun)]),
530                world: None,
531            }))
532        } else {
533            Ok(self.ctx.exprs.alloc(LogicExpr::Atom(subject.noun)))
534        }
535    }
536
537    fn parse_scopal_adverb(&mut self, subject: &NounPhrase<'a>) -> ParseResult<&'a LogicExpr<'a>> {
538        let operator = if let TokenType::ScopalAdverb(adv) = self.advance().kind.clone() {
539            adv
540        } else {
541            return Err(ParseError {
542                kind: ParseErrorKind::ExpectedScopalAdverb,
543                span: self.current_span(),
544            });
545        };
546
547        if !self.check_verb() {
548            return Err(ParseError {
549                kind: ParseErrorKind::ExpectedVerb {
550                    found: self.peek().kind.clone(),
551                },
552                span: self.current_span(),
553            });
554        }
555
556        // A bare verb keeps the simple predication shape; anything after it
557        // ("almost killed Mary") takes the full VP grammar under the operator.
558        let clause_ends_after_verb = matches!(
559            self.tokens.get(self.current + 1).map(|t| t.kind.clone()),
560            Some(
561                TokenType::Period
562                    | TokenType::Exclamation
563                    | TokenType::EOF
564                    | TokenType::Comma
565                    | TokenType::And
566            ) | None
567        );
568        if !clause_ends_after_verb {
569            use super::verb::LogicVerbParsing;
570            let body = self.parse_predicate_with_subject(subject.noun)?;
571            return Ok(self.ctx.exprs.alloc(LogicExpr::Scopal { operator, body }));
572        }
573
574        let (verb, verb_time, _verb_aspect, _) = self.consume_verb_with_metadata();
575
576        let predicate = self.ctx.exprs.alloc(LogicExpr::Predicate {
577            name: verb,
578            args: self.ctx.terms.alloc_slice([Term::Constant(subject.noun)]),
579            world: None,
580        });
581
582        let with_time = match verb_time {
583            Time::Past => self.ctx.exprs.alloc(LogicExpr::Temporal {
584                operator: TemporalOperator::Past,
585                body: predicate,
586            }),
587            Time::Future => self.ctx.exprs.alloc(LogicExpr::Temporal {
588                operator: TemporalOperator::Future,
589                body: predicate,
590            }),
591            _ => predicate,
592        };
593
594        Ok(self.ctx.exprs.alloc(LogicExpr::Scopal {
595            operator,
596            body: with_time,
597        }))
598    }
599
600    fn parse_superlative(&mut self, subject: &NounPhrase<'a>) -> ParseResult<&'a LogicExpr<'a>> {
601        let adj = if let TokenType::Superlative(adj) = self.advance().kind.clone() {
602            adj
603        } else {
604            return Err(ParseError {
605                kind: ParseErrorKind::ExpectedSuperlativeAdjective,
606                span: self.current_span(),
607            });
608        };
609
610        let domain = self.consume_content_word()?;
611
612        Ok(self.ctx.exprs.alloc(LogicExpr::Superlative {
613            adjective: adj,
614            subject: self.ctx.terms.alloc(Term::Constant(subject.noun)),
615            domain,
616        }))
617    }
618
619    fn parse_comparative(
620        &mut self,
621        subject: &NounPhrase<'a>,
622        _copula_time: Time,
623        difference: Option<&'a Term<'a>>,
624    ) -> ParseResult<&'a LogicExpr<'a>> {
625        // A degree adverb ("somewhat shorter", "slightly wider", "much taller")
626        // may precede the comparative; it stresses the gap but adds no measurable
627        // offset, so skip it — the strict inequality the comparative yields already
628        // captures "more than, by an unspecified amount". Degree adverbs are a
629        // closed lexical class (lexicon `degree_adverbs`).
630        if crate::lexicon::is_degree_adverb(
631            &self.interner.resolve(self.peek().lexeme).to_lowercase(),
632        ) && matches!(
633            self.tokens.get(self.current + 1).map(|t| &t.kind),
634            Some(TokenType::Comparative(_))
635        ) {
636            self.advance(); // degree adverb
637        }
638
639        let comp_tok = self.advance().clone();
640        let comp_surface = self.interner.resolve(comp_tok.lexeme).to_string();
641        let adj = if let TokenType::Comparative(adj) = comp_tok.kind {
642            adj
643        } else {
644            return Err(ParseError {
645                kind: ParseErrorKind::ExpectedComparativeAdjective,
646                span: self.current_span(),
647            });
648        };
649
650        if !self.check(&TokenType::Than) {
651            // A bare comparative predicate with no standard ("X is older", "one is
652            // taller", "the other is faster") — the comparative adjective is a
653            // unary property relative to a contextually-implied standard (the
654            // other entity in a pair). Build Older(X) — the COMPARATIVE surface
655            // ("older"), not the base lemma ("Old"), so the degree isn't lost —
656            // rather than failing; the constraint is preserved (zero meaning
657            // loss), and in an of-pair / list context the complementary
658            // predicates pair up for the prover.
659            let comp_name = {
660                let mut c = comp_surface.chars();
661                match c.next() {
662                    Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
663                    None => comp_surface.clone(),
664                }
665            };
666            return Ok(self.ctx.exprs.alloc(LogicExpr::Predicate {
667                name: self.interner.intern(&comp_name),
668                args: self.ctx.terms.alloc_slice([Term::Constant(subject.noun)]),
669                world: None,
670            }));
671        }
672        self.advance();
673
674        // Check if the comparison target is a number (e.g., "greater than 0")
675        let object_term = if self.check_number() {
676            // Parse number as the comparison target
677            let num_sym = if let TokenType::Number(sym) = self.advance().kind {
678                sym
679            } else {
680                unreachable!()
681            };
682            let num_str = self.interner.resolve(num_sym);
683            let num_val = num_str.parse::<i64>().unwrap_or(0);
684            self.ctx.terms.alloc(Term::Value {
685                kind: crate::ast::logic::NumberKind::Integer(num_val),
686                unit: None,
687                dimension: None,
688            })
689        } else {
690            // Parse noun phrase as the comparison target — GREEDY so the standard's
691            // PPs / reduced relatives attach ("shorter than the figure WITH THE
692            // YELLOW HAT", "smaller than the tank GOING TO PHILO"). A "than"
693            // standard is nominal — a verb-word head there is a deverbal noun
694            // ("larger than the orange PACK").
695            let saved_ctx = self.nominal_np_context;
696            self.nominal_np_context = true;
697            let object_result = self.parse_noun_phrase(true);
698            self.nominal_np_context = saved_ctx;
699            let object = object_result?;
700
701            // Comparative subdeletion (§2.4): a clausal than-complement with its OWN
702            // gradable dimension — "than the door is WIDE" → compare the matrix
703            // length-degree to the than-clause width-degree:
704            //   ∃d∃d'(Long(desk,d) ∧ Wide(door,d') ∧ d > d').
705            if matches!(
706                self.peek().kind,
707                TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were
708            ) {
709                let save = self.current;
710                self.advance(); // copula
711                if let TokenType::Adjective(adj2) = self.peek().kind {
712                    self.advance();
713                    let d1 = self.next_var_name();
714                    let d2 = self.next_var_name();
715                    let matrix = self.ctx.exprs.alloc(LogicExpr::Predicate {
716                        name: adj,
717                        args: self
718                            .ctx
719                            .terms
720                            .alloc_slice([Term::Constant(subject.noun), Term::Variable(d1)]),
721                        world: None,
722                    });
723                    let than_clause = self.ctx.exprs.alloc(LogicExpr::Predicate {
724                        name: adj2,
725                        args: self
726                            .ctx
727                            .terms
728                            .alloc_slice([Term::Constant(object.noun), Term::Variable(d2)]),
729                        world: None,
730                    });
731                    let gt = self.ctx.exprs.alloc(LogicExpr::Predicate {
732                        name: self.interner.intern(">"),
733                        args: self
734                            .ctx
735                            .terms
736                            .alloc_slice([Term::Variable(d1), Term::Variable(d2)]),
737                        world: None,
738                    });
739                    let conj1 = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
740                        left: matrix,
741                        op: TokenType::And,
742                        right: than_clause,
743                    });
744                    let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
745                        left: conj1,
746                        op: TokenType::And,
747                        right: gt,
748                    });
749                    let inner = self.ctx.exprs.alloc(LogicExpr::Quantifier {
750                        kind: crate::ast::logic::QuantifierKind::Existential,
751                        variable: d2,
752                        body,
753                        island_id: self.current_island,
754                    });
755                    let outer = self.ctx.exprs.alloc(LogicExpr::Quantifier {
756                        kind: crate::ast::logic::QuantifierKind::Existential,
757                        variable: d1,
758                        body: inner,
759                        island_id: self.current_island,
760                    });
761                    return Ok(outer);
762                }
763                // Clausal ellipsis ("taller than Bill is."): the bare copula
764                // adds nothing — keep it consumed at a clause boundary,
765                // otherwise restore for other readings.
766                if self.at_clause_boundary() {
767                    // copula consumed; comparison proceeds as phrasal
768                } else {
769                    self.current = save;
770                }
771            }
772
773            // SUBJECT side: a descriptive subject (adjectives / possessor / PPs / a
774            // reduced relative) becomes a DISTINCT existential entity carrying its
775            // restrictor — mirroring the standard side — so "the fall Derrick
776            // photographed in 1987 is shorter than …" keeps the reduced relative on
777            // the subject instead of collapsing to the bare head constant. The
778            // variable is used DIRECTLY in the comparison (no later substitution
779            // needed); a bare-head subject stays a constant under the definiteness wrap.
780            let subj_is_desc = !subject.adjectives.is_empty()
781                || subject.possessor.is_some()
782                || !subject.pps.is_empty();
783            let subj_var = if subj_is_desc { Some(self.next_var_name()) } else { None };
784            let subj_term = match subj_var {
785                Some(sv) => Term::Variable(sv),
786                None => Term::Constant(subject.noun),
787            };
788
789            // A standard carrying restrictions (adjectives, possessor, PPs, or a
790            // who/that relative clause) becomes a DISTINCT existential entity with a
791            // restrictor so nothing is dropped — same distinct-identity pattern as
792            // the arithmetic comparative / of-pair. A bare name/definite-head stays
793            // a constant.
794            let has_rel = self.check(&TokenType::Who) || self.check(&TokenType::That);
795            let obj_is_desc = has_rel
796                || !object.adjectives.is_empty()
797                || object.possessor.is_some()
798                || !object.pps.is_empty();
799            if obj_is_desc {
800                let obj_var = self.next_var_name();
801                let obj_var_term = Term::Variable(obj_var);
802                let mut restrictor = self.nominal_predication_with_pps(obj_var_term, &object);
803                if has_rel {
804                    self.advance(); // "who" / "that"
805                    let rel = self.parse_relative_clause(obj_var)?;
806                    restrictor = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
807                        left: restrictor,
808                        op: TokenType::And,
809                        right: rel,
810                    });
811                }
812                let cmp = self.ctx.exprs.alloc(LogicExpr::Comparative {
813                    adjective: adj,
814                    subject: self.ctx.terms.alloc(subj_term),
815                    object: self.ctx.terms.alloc(obj_var_term),
816                    difference,
817                    relation: crate::ast::ComparisonRelation::Greater,
818                });
819                let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
820                    left: restrictor,
821                    op: TokenType::And,
822                    right: cmp,
823                });
824                let quant = self.ctx.exprs.alloc(LogicExpr::Quantifier {
825                    kind: crate::ast::logic::QuantifierKind::Existential,
826                    variable: obj_var,
827                    body,
828                    island_id: self.current_island,
829                });
830                return match subj_var {
831                    Some(sv) => {
832                        let subj_restrictor =
833                            self.nominal_predication_with_pps(Term::Variable(sv), subject);
834                        let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
835                            left: subj_restrictor,
836                            op: TokenType::And,
837                            right: quant,
838                        });
839                        Ok(self.ctx.exprs.alloc(LogicExpr::Quantifier {
840                            kind: crate::ast::logic::QuantifierKind::Existential,
841                            variable: sv,
842                            body,
843                            island_id: self.current_island,
844                        }))
845                    }
846                    None => self.wrap_with_definiteness(subject.definiteness, subject.noun, quant),
847                };
848            }
849
850            let obj_term = self.ctx.terms.alloc(Term::Constant(object.noun));
851
852            let result = self.ctx.exprs.alloc(LogicExpr::Comparative {
853                adjective: adj,
854                subject: self.ctx.terms.alloc(subj_term),
855                object: obj_term,
856                difference,
857                relation: crate::ast::ComparisonRelation::Greater,
858            });
859
860            let result = match subj_var {
861                Some(sv) => {
862                    let subj_restrictor =
863                        self.nominal_predication_with_pps(Term::Variable(sv), subject);
864                    let body = self.ctx.exprs.alloc(LogicExpr::BinaryOp {
865                        left: subj_restrictor,
866                        op: TokenType::And,
867                        right: result,
868                    });
869                    self.ctx.exprs.alloc(LogicExpr::Quantifier {
870                        kind: crate::ast::logic::QuantifierKind::Existential,
871                        variable: sv,
872                        body,
873                        island_id: self.current_island,
874                    })
875                }
876                None => self.wrap_with_definiteness(subject.definiteness, subject.noun, result)?,
877            };
878            return self.wrap_with_definiteness_for_object(object.definiteness, object.noun, result);
879        };
880
881        // For number comparisons, create a simple Comparative expression
882        Ok(self.ctx.exprs.alloc(LogicExpr::Comparative {
883            adjective: adj,
884            subject: self.ctx.terms.alloc(Term::Constant(subject.noun)),
885            object: object_term,
886            difference,
887            relation: crate::ast::ComparisonRelation::Greater,
888        }))
889    }
890
891    /// Parses the equative frame "X is as ADJ as Y" → an at-least (`≥`) degree
892    /// comparison. The parser is positioned at the first "as"; the subject's
893    /// definiteness wrapping is applied by the caller.
894    fn parse_equative(
895        &mut self,
896        subject: &NounPhrase<'a>,
897    ) -> ParseResult<&'a LogicExpr<'a>> {
898        self.advance(); // consume the first "as"
899        // The gradable dimension (an adjective).
900        let adj = match self.advance().kind.clone() {
901            TokenType::Adjective(a) => a,
902            TokenType::Comparative(a) => a,
903            other => {
904                // Use the lexeme as the dimension if it is not a plain adjective token.
905                if let TokenType::Noun(a) = other {
906                    a
907                } else {
908                    self.interner.intern(&self.interner.resolve(self.previous().lexeme).to_string())
909                }
910            }
911        };
912        // Second "as".
913        if self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("as") {
914            self.advance();
915        }
916        let object = self.parse_noun_phrase(false)?;
917        let obj_term = self.ctx.terms.alloc(Term::Constant(object.noun));
918        let result = self.ctx.exprs.alloc(LogicExpr::Comparative {
919            adjective: adj,
920            subject: self.ctx.terms.alloc(Term::Constant(subject.noun)),
921            object: obj_term,
922            difference: None,
923            relation: crate::ast::ComparisonRelation::GreaterEqual,
924        });
925        let result = self.wrap_with_definiteness(subject.definiteness, subject.noun, result)?;
926        self.wrap_with_definiteness_for_object(object.definiteness, object.noun, result)
927    }
928
929    fn check_number(&self) -> bool {
930        // A clock time ("9:30am", "8:15 pm") is a measure-like value too — it names
931        // a point on the day's timeline that the prover can order, so it is a valid
932        // measure-phrase / PP object ("is at 9:30am", "the meeting at 8:15 pm").
933        matches!(
934            self.peek().kind,
935            TokenType::Number(_) | TokenType::TimeLiteral { .. }
936        )
937    }
938
939    fn parse_measure_phrase(&mut self) -> ParseResult<&'a Term<'a>> {
940        // A clock-time literal is a time-of-day VALUE the prover can order against
941        // other times — represented as minutes-from-midnight (an integer on the
942        // day's timeline) tagged with a `ClockTime` dimension, so "is at 9:30am"
943        // and "the 8:15 pm event" compare numerically. (TODO: timezone-aware times
944        // — carry an offset/zone on the value — when the corpus needs them.)
945        if let TokenType::TimeLiteral { nanos_from_midnight } = self.peek().kind {
946            self.advance();
947            let minutes = (nanos_from_midnight / 60_000_000_000) as i64;
948            return Ok(self.ctx.terms.alloc(Term::Value {
949                kind: crate::ast::logic::NumberKind::Integer(minutes),
950                unit: None,
951                dimension: Some(crate::ast::logic::Dimension::Time),
952            }));
953        }
954        let num_sym = if let TokenType::Number(sym) = self.advance().kind {
955            sym
956        } else {
957            return Err(ParseError {
958                kind: ParseErrorKind::ExpectedNumber,
959                span: self.current_span(),
960            });
961        };
962
963        let num_str = self.interner.resolve(num_sym);
964        let kind = parse_number_kind(num_str, num_sym);
965
966        // The unit noun after the number ("385 degrees", "5 dollars", "190
967        // points"). An INFLECTED verb (past/future) is never a unit — it is the
968        // matrix predicate ("issued in 1850 sold …" must keep "sold" as the verb,
969        // not read "1850 sold" as a measure), so it is left unconsumed.
970        let next_is_inflected_verb = match &self.peek().kind {
971            TokenType::Verb { time, .. } => matches!(time, Time::Past | Time::Future),
972            TokenType::Ambiguous { primary, .. } => {
973                matches!(**primary, TokenType::Verb { time, .. } if matches!(time, Time::Past | Time::Future))
974            }
975            _ => false,
976        };
977        let (unit, dimension) = if matches!(self.peek().kind, TokenType::CalendarUnit(_)) {
978            // A calendar unit ("years", "months", "days") is a measure unit too:
979            // "12 years old", "3 weeks late". (The COUNT-UNIT-after/before temporal
980            // OFFSET is caught earlier in try_temporal_offset, so this only fires
981            // for the non-offset measure use.)
982            let unit_word = self.peek().lexeme;
983            self.advance();
984            (Some(unit_word), None)
985        } else if self.check_content_word() && !self.check_article() && !next_is_inflected_verb {
986            // An ARTICLE after the number is never the unit — it starts a rate
987            // denominator ("$700 A month") or a following NP; leave it unconsumed.
988            let unit_word = self.consume_content_word()?;
989            let unit_str = self.interner.resolve(unit_word).to_lowercase();
990            let dim = lexicon::lookup_unit_dimension(&unit_str);
991            (Some(unit_word), dim)
992        } else {
993            (None, None)
994        };
995
996        Ok(self.ctx.terms.alloc(Term::Value { kind, unit, dimension }))
997    }
998
999    fn counting_np_lookahead(&self) -> Option<u32> {
1000        let n = match self.peek().kind {
1001            TokenType::Number(sym) => self.interner.resolve(sym).parse::<u32>().ok()?,
1002            _ => return None,
1003        };
1004        // Scan ≥1 modifier (adjective or a ProperName premodifier), then require a
1005        // common-noun head. The modifier is what proves this is a count — "6 BROWN
1006        // manatees", "640 TWITTER followers", "78 LINKEDIN connections" — not a
1007        // measure ("190 points" has no modifier and stays on the measure path).
1008        let mut i = self.current + 1;
1009        let mut saw_modifier = false;
1010        while matches!(
1011            self.tokens.get(i).map(|t| &t.kind),
1012            Some(TokenType::Adjective(_))
1013                | Some(TokenType::NonIntersectiveAdjective(_))
1014                | Some(TokenType::ProperName(_))
1015        ) {
1016            saw_modifier = true;
1017            i += 1;
1018        }
1019        if !saw_modifier {
1020            return None;
1021        }
1022        let head_is_noun = match self.tokens.get(i).map(|t| &t.kind) {
1023            Some(TokenType::Noun(_)) | Some(TokenType::Item) | Some(TokenType::Items) => true,
1024            // A verb-word head ("49 previous JUMPS", "six previous RUNS") is a
1025            // DEVERBAL NOUN here — a number followed by an adjective cannot
1026            // precede a finite verb, so the head is nominal. The object-NP path
1027            // recovers it as the head (via nominal_np_context).
1028            Some(TokenType::Verb { .. }) => true,
1029            Some(TokenType::Ambiguous { primary, alternatives }) => {
1030                matches!(**primary, TokenType::Noun(_) | TokenType::Verb { .. })
1031                    || alternatives
1032                        .iter()
1033                        .any(|t| matches!(t, TokenType::Noun(_) | TokenType::Verb { .. }))
1034            }
1035            _ => false,
1036        };
1037        head_is_noun.then_some(n)
1038    }
1039}
1040
1041fn parse_number_kind(s: &str, sym: crate::intern::Symbol) -> NumberKind {
1042    if s.contains('.') {
1043        NumberKind::Real(s.parse().unwrap_or(0.0))
1044    } else if s.chars().all(|c| c.is_ascii_digit() || c == '-') {
1045        NumberKind::Integer(s.parse().unwrap_or(0))
1046    } else {
1047        NumberKind::Symbolic(sym)
1048    }
1049}