logicaffeine_language/parser/noun.rs
1//! Noun phrase parsing with determiners, adjectives, and possessives.
2//!
3//! This module handles the full complexity of English noun phrases including:
4//!
5//! - **Determiners**: Articles (a, the), quantifiers (every, some, no)
6//! - **Adjectives**: Pre-nominal modifiers, intersective vs subsective
7//! - **Possessives**: "John's", "his", genitive constructions
8//! - **Proper names**: Capitalized constants
9//! - **Numeric literals**: Numbers as noun phrases for comparisons
10//! - **Prepositional phrases**: Post-nominal "of" constructions
11//! - **Superlatives**: "the tallest", "the most interesting"
12//!
13//! The parsed [`NounPhrase`] struct carries definiteness, adjectives, the head
14//! noun, optional possessor, and attached prepositional phrases.
15
16use super::clause::ClauseParsing;
17use super::pragmatics::PragmaticsParsing;
18use super::{ParseResult, Parser};
19use crate::ast::{LogicExpr, NounPhrase, Term};
20use crate::drs::{Case, Gender, Number};
21use logicaffeine_base::SymbolEq;
22use crate::lexicon::Definiteness;
23use crate::token::TokenType;
24use crate::transpile::capitalize_first;
25
26/// The preposition a declared CATEGORY contributes to a definite-description
27/// label, so the label un-fuses to the SAME relation the prepositional-phrase
28/// form produces. TEMPORAL ("the 2003 holiday" ↔ "in 2003") and LOCATIVE ("the
29/// Florida trip" ↔ "in Florida") both → `In`; PERSONAL ("the Bill trip" ↔ "with
30/// Bill") → `With`.
31///
32/// The lexicon SORT is consulted first where it is decisive (`Place`/`Time` →
33/// `In`, `Human` → `With`), but several calendar/region nouns carry an
34/// `Abstract` sort in the lexicon (year, month, week, decade, century, state),
35/// so an explicit lemma set backs the sort up. A category that maps to neither
36/// dimension (e.g. "vegetable") returns `None` and the label stays fused.
37fn category_preposition(category_lemma_lower: &str) -> Option<&'static str> {
38 const TEMPORAL: &[&str] = &["year", "month", "day", "date", "week", "decade", "century"];
39 const LOCATIVE: &[&str] =
40 &["state", "city", "country", "province", "region", "town", "place"];
41 const PERSONAL: &[&str] =
42 &["friend", "person", "man", "woman", "colleague", "companion"];
43
44 if TEMPORAL.contains(&category_lemma_lower) || LOCATIVE.contains(&category_lemma_lower) {
45 return Some("In");
46 }
47 if PERSONAL.contains(&category_lemma_lower) {
48 return Some("With");
49 }
50 match crate::lexicon::lookup_sort(category_lemma_lower) {
51 Some(crate::lexicon::Sort::Place) | Some(crate::lexicon::Sort::Time) => Some("In"),
52 Some(crate::lexicon::Sort::Human) => Some("With"),
53 _ => None,
54 }
55}
56
57/// Trait for parsing noun phrases.
58///
59/// Provides methods for parsing determiners, adjectives, possessives,
60/// and converting noun phrases to first-order terms.
61pub trait NounParsing<'a, 'ctx, 'int> {
62 /// Parses a full noun phrase with optional greedy PP attachment.
63 fn parse_noun_phrase(&mut self, greedy: bool) -> ParseResult<NounPhrase<'a>>;
64 /// Parses a noun phrase suitable for relative clause antecedent.
65 fn parse_noun_phrase_for_relative(&mut self) -> ParseResult<NounPhrase<'a>>;
66 /// Converts a parsed noun phrase to a first-order term.
67 fn noun_phrase_to_term(&self, np: &NounPhrase<'a>) -> Term<'a>;
68 /// Checks for possessive marker ('s).
69 fn check_possessive(&self) -> bool;
70 /// Checks for "of" preposition (possessive or partitive).
71 fn check_of_preposition(&self) -> bool;
72 /// Checks for proper name or label (capitalized).
73 fn check_proper_name_or_label(&self) -> bool;
74 /// Whether the cursor opens an object-gap reduced relative (subject + transitive
75 /// verb + empty object slot), e.g. "Tara won" in "the prize Tara won".
76 fn peek_reduced_object_relative(&self) -> bool;
77 /// Whether the cursor sits on a DEFINITE article that opens a noun phrase whose
78 /// head is modified by a reduced object relative ("the friend Simon went with",
79 /// "the waterfall Derrick photographed"). Used by the object-NP dispatcher to
80 /// route such an object through the full `parse_noun_phrase` machinery instead
81 /// of pre-consuming the article (which would hide the relative).
82 fn peek_definite_reduced_relative_object(&self) -> bool;
83 /// Checks for possessive pronoun (his, her, its, their).
84 fn check_possessive_pronoun(&self) -> bool;
85 /// Resolves a numeric LABEL ("the 2003 holiday", "the 1850 stamp"): returns
86 /// the head symbol, FUSED (`2003_holiday`) by default, but UN-FUSED to the
87 /// bare head plus a category relation restrictor (pushed onto
88 /// `measure_restrictors`) when `n` names a DRS-declared item whose category
89 /// maps to a preposition.
90 fn numeric_label_head(
91 &mut self,
92 n: i64,
93 head: crate::intern::Symbol,
94 definiteness: Option<Definiteness>,
95 measure_restrictors: &mut Vec<&'a LogicExpr<'a>>,
96 ) -> crate::intern::Symbol;
97 /// Consume a numeric-label HEAD preferring the NOUN reading of a verb-ambiguous
98 /// word, so the fused symbol matches the noun-compound form ("the 2001 trip" →
99 /// `2001_trip`, not the verb lemma `2001_Trip`). A verb-only head ("stamp")
100 /// has no noun reading and keeps its lemma; a plain noun ("holiday") is already
101 /// the noun. (The un-fused predicate is capitalized downstream regardless.)
102 fn consume_label_head_noun_first(&mut self) -> ParseResult<crate::intern::Symbol>;
103}
104
105impl<'a, 'ctx, 'int> NounParsing<'a, 'ctx, 'int> for Parser<'a, 'ctx, 'int> {
106 fn parse_noun_phrase(&mut self, greedy: bool) -> ParseResult<NounPhrase<'a>> {
107 let mut definiteness = None;
108 let mut adjectives = Vec::new();
109 let mut non_intersective_prefix: Option<crate::intern::Symbol> = None;
110 let mut possessor_from_pronoun: Option<&'a NounPhrase<'a>> = None;
111 let mut superlative_adj: Option<crate::intern::Symbol> = None;
112 // Attributive measure-adjective restrictors ("the 80 year old doll" →
113 // Old(_PP_SELF_, 80 years)); merged into the NP's pps after the head, so a
114 // degree property survives in every position the pps flow through.
115 let mut measure_restrictors: Vec<&'a LogicExpr<'a>> = Vec::new();
116
117 // Phase 35: Support numeric literals as noun phrases (e.g., "equal to 42").
118 // BUT only when the number stands alone — a number FOLLOWED by a content
119 // word heads a larger NP ("28 inch wingspan", "640 Twitter followers"),
120 // which the numeric-head compound logic below folds; early-returning here
121 // would strand the rest ("inch wingspan").
122 if let TokenType::Number(sym) = self.peek().kind {
123 let number_modifies_head = matches!(
124 self.tokens.get(self.current + 1).map(|t| &t.kind),
125 Some(TokenType::Noun(_)) | Some(TokenType::ProperName(_))
126 | Some(TokenType::Adjective(_)) | Some(TokenType::Verb { .. })
127 | Some(TokenType::Ambiguous { .. })
128 );
129 if !number_modifies_head {
130 self.advance();
131 return Ok(NounPhrase {
132 definiteness: None,
133 adjectives: &[],
134 noun: sym,
135 possessor: None,
136 pps: &[],
137 superlative: None,
138 });
139 }
140 }
141
142 if self.check_possessive_pronoun() {
143 let token = self.advance().clone();
144 let (gender, number) = match &token.kind {
145 TokenType::Pronoun { gender, number, case: Case::Possessive } => (*gender, *number),
146 TokenType::Ambiguous { primary, alternatives } => {
147 let mut found = None;
148 if let TokenType::Pronoun { gender, number, case: Case::Possessive } = **primary {
149 found = Some((gender, number));
150 }
151 if found.is_none() {
152 for alt in alternatives {
153 if let TokenType::Pronoun { gender, number, case: Case::Possessive } = alt {
154 found = Some((*gender, *number));
155 break;
156 }
157 }
158 }
159 found.unwrap_or((Gender::Unknown, Number::Singular))
160 }
161 _ => (Gender::Unknown, Number::Singular),
162 };
163
164 let resolved = self.resolve_pronoun(gender, number)?;
165 let resolved_sym = match resolved {
166 super::ResolvedPronoun::Variable(s) | super::ResolvedPronoun::Constant(s) => s,
167 };
168
169 let possessor_np = NounPhrase {
170 definiteness: None,
171 adjectives: &[],
172 noun: resolved_sym,
173 possessor: None,
174 pps: &[],
175 superlative: None,
176 };
177 possessor_from_pronoun = Some(self.ctx.nps.alloc(possessor_np));
178 definiteness = Some(Definiteness::Definite);
179 } else if let TokenType::Article(def) = self.peek().kind {
180 // Phase 35: Disambiguate "a" as variable vs article
181 // If "a" or "an" is followed by a verb/copula/modal, it's a variable name, not an article
182 let is_variable_a = {
183 let lexeme = self.interner.resolve(self.peek().lexeme).to_lowercase();
184 if lexeme == "a" || lexeme == "an" {
185 if let Some(next) = self.tokens.get(self.current + 1) {
186 // A PROGRESSIVE verb (an "-ing" gerund) directly before a
187 // noun head is a PRE-NOMINAL MODIFIER, not a predicate — so
188 // "a kayaking REGIMEN", "a running SHOE" are indefinite NPs
189 // and "a" is an article, not a logic variable. Only this
190 // gerund+noun shape is excluded; "a runs"/"a sees Bob"/"a =
191 // b" keep the variable reading.
192 let gerund_premodifier = matches!(
193 next.kind,
194 TokenType::Verb { aspect: crate::lexicon::Aspect::Progressive, .. }
195 ) && matches!(
196 self.tokens.get(self.current + 2).map(|t| &t.kind),
197 Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
198 );
199 !gerund_premodifier && matches!(next.kind,
200 TokenType::Is | TokenType::Are | TokenType::Was | TokenType::Were | // Copula
201 TokenType::Verb { .. } | // Main verb
202 TokenType::Auxiliary(_) | // will, did
203 TokenType::Must | TokenType::Can | TokenType::Should | TokenType::May | // Modals
204 TokenType::Could | TokenType::Would | TokenType::Shall | TokenType::Might |
205 TokenType::Identity | TokenType::Equals // "a = b"
206 )
207 } else {
208 false
209 }
210 } else {
211 false
212 }
213 };
214
215 if !is_variable_a {
216 definiteness = Some(def);
217 self.advance();
218 }
219 }
220
221 if self.check_superlative() {
222 if let TokenType::Superlative(adj) = self.advance().kind {
223 superlative_adj = Some(adj);
224 }
225 }
226
227 if self.check_non_intersective_adjective() {
228 if let TokenType::NonIntersectiveAdjective(adj) = self.advance().kind {
229 non_intersective_prefix = Some(adj);
230 }
231 }
232
233 loop {
234 if self.is_at_end() {
235 break;
236 }
237
238 // A gerund (-ing form) directly before a noun head in a DEFINITE
239 // description is an attributive MODIFIER, not part of the head: "the
240 // hunting trip" → Trip(x) ∧ Hunt(x). Keeping the attribute as a
241 // first-class predicate (un-fused) means "hunting" is the same key
242 // wherever the entity is named ("the hunting vacation", "the hunting
243 // trip"), so the discourse layer can resolve them to one referent.
244 // Restricted to definites because reference-resolution applies to
245 // definite descriptions; a bare-plural object ("used bowling pins")
246 // becomes a constant with no restrictor to hold a separate predicate,
247 // so its modifier must stay folded into the head symbol (Bowl_pins).
248 if let TokenType::Verb {
249 lemma,
250 aspect: crate::lexicon::Aspect::Progressive,
251 ..
252 } = self.peek().kind
253 {
254 let next_is_head = matches!(
255 self.tokens.get(self.current + 1).map(|t| &t.kind),
256 Some(TokenType::Noun(_))
257 | Some(TokenType::ProperName(_))
258 | Some(TokenType::Ambiguous { .. })
259 );
260 if next_is_head && definiteness == Some(Definiteness::Definite) {
261 self.advance();
262 adjectives.push(lemma);
263 continue;
264 }
265 }
266
267 // A proper name directly before a common-noun head in a DEFINITE
268 // description is an attributive LABEL modifier, not part of the head:
269 // "the Florida trip" → Trip(x) ∧ Florida(x); "the Woodard family" →
270 // Family(x) ∧ Woodard(x). Un-fusing keeps the label a first-class key
271 // so the discourse layer resolves "the Florida trip" and "the Florida
272 // vacation" (or "the Woodard family" and "the Woodard estate") to one
273 // referent. Definite-gated, so a bare named entity ("Lake Tahoe",
274 // "Leiman Manor") stays a single constant and never un-fuses.
275 if let TokenType::ProperName(label) = self.peek().kind {
276 let next_is_common_head = matches!(
277 self.tokens.get(self.current + 1).map(|t| &t.kind),
278 Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
279 );
280 if next_is_common_head && definiteness == Some(Definiteness::Definite) {
281 self.advance();
282 // A proper name that names a DECLARED item ("Florida"
283 // registered as a state, "Bill" as a friend) un-fuses to a
284 // P(_PP_SELF_, <name>) restrictor instead of a bare predicate,
285 // converging with the prepositional-phrase form ("the trip was
286 // in Florida" → In(x, Florida); "…with Bill" → With(x, Bill)).
287 // The object term mirrors the PP form's constant EXACTLY so the
288 // two unify. An undeclared label ("the Woodard family") keeps
289 // its bare-predicate behavior.
290 let unfused = self.drs.item_category(label).and_then(|category| {
291 let cat_lemma = Self::singularize_noun(self.interner.resolve(category));
292 category_preposition(&cat_lemma.to_lowercase())
293 });
294 if let Some(prep) = unfused {
295 let prep_sym = self.interner.intern(prep);
296 let placeholder = self.interner.intern("_PP_SELF_");
297 let pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
298 name: prep_sym,
299 args: self
300 .ctx
301 .terms
302 .alloc_slice([Term::Variable(placeholder), Term::Constant(label)]),
303 world: None,
304 });
305 measure_restrictors.push(pred);
306 } else {
307 adjectives.push(label);
308 }
309 continue;
310 }
311 }
312
313 // Compound color / quality term: a NOUN immediately before an
314 // ADJECTIVE that is in turn followed by a HEAD NOUN ("LIME green
315 // SHIRT", "MIDNIGHT blue CAR", "BLOOD red CAR") — the noun pre-modifies
316 // the adjective; fold the pair into one "Lime_green" adjective. The
317 // following head-noun requirement distinguishes this from a head noun
318 // + a post-nominal secondary predicate ("painted the DOOR red.", where
319 // "door" is the head and "red" a resultative): there no noun follows.
320 let color_compound = matches!(self.peek().kind, TokenType::Noun(_))
321 && matches!(
322 self.tokens.get(self.current + 1).map(|t| &t.kind),
323 Some(TokenType::Adjective(_))
324 )
325 && matches!(
326 self.tokens.get(self.current + 2).map(|t| &t.kind),
327 Some(TokenType::Noun(_))
328 | Some(TokenType::ProperName(_))
329 | Some(TokenType::Adjective(_))
330 | Some(TokenType::Verb { .. })
331 | Some(TokenType::Ambiguous { .. })
332 );
333 if color_compound {
334 let n = if let TokenType::Noun(n) = self.peek().kind { n } else { unreachable!() };
335 self.advance(); // the noun modifier
336 let a = if let TokenType::Adjective(a) = self.peek().kind { a } else { unreachable!() };
337 self.advance(); // the adjective
338 let compound = self.interner.intern(&format!(
339 "{}_{}",
340 self.interner.resolve(n),
341 self.interner.resolve(a)
342 ));
343 adjectives.push(compound);
344 continue;
345 }
346
347 // Attributive measure-adjective: "80 year old [doll]", "28 inch long
348 // [wing]" — a measure phrase (Number + unit) modifying a following
349 // gradable ADJECTIVE is a degree property of the head, mirroring the
350 // predicative "is 80 years old" → Old(x, 80 years). Emit
351 // Adj(_PP_SELF_, value) so the degree AND its unit survive rather than
352 // stranding the unit token. Gated on the third token being an ADJECTIVE,
353 // so "28 inch wingspan" (a measure-noun compound) is left to the head.
354 let measure_num = match self.peek().kind {
355 TokenType::Cardinal(n) => Some(crate::ast::logic::NumberKind::Integer(n as i64)),
356 TokenType::Number(s) => {
357 let raw = self.interner.resolve(s).replace(',', "");
358 Some(
359 raw.parse::<i64>()
360 .map(crate::ast::logic::NumberKind::Integer)
361 .unwrap_or(crate::ast::logic::NumberKind::Symbolic(s)),
362 )
363 }
364 _ => None,
365 };
366 if let Some(kind) = measure_num {
367 let unit_tok = self.tokens.get(self.current + 1);
368 let unit_is_measure = unit_tok.map_or(false, |t| {
369 matches!(t.kind, TokenType::CalendarUnit(_))
370 || matches!(t.kind, TokenType::Noun(_)
371 if crate::lexicon::lookup_unit_dimension(
372 &self.interner.resolve(t.lexeme).to_lowercase()).is_some())
373 });
374 let adj_after = self.tokens.get(self.current + 2).and_then(|t| {
375 if let TokenType::Adjective(a) = t.kind { Some(a) } else { None }
376 });
377 if unit_is_measure {
378 if let Some(adj) = adj_after {
379 let unit_sym = unit_tok.unwrap().lexeme;
380 self.advance(); // number
381 self.advance(); // unit
382 self.advance(); // gradable adjective
383 let placeholder = self.interner.intern("_PP_SELF_");
384 let value = Term::Value { kind, unit: Some(unit_sym), dimension: None };
385 let pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
386 name: adj,
387 args: self
388 .ctx
389 .terms
390 .alloc_slice([Term::Variable(placeholder), value]),
391 world: None,
392 });
393 measure_restrictors.push(pred);
394 continue;
395 }
396 }
397 }
398
399 let is_adjective = matches!(self.peek().kind, TokenType::Adjective(_));
400 if !is_adjective {
401 break;
402 }
403
404 // A verb-only or ambiguous word after an adjective is the head noun
405 // ("the rare stamp", "the long run") — but ONLY inside a
406 // determiner-headed NP. Without the article gate, "studies hard
407 // pass …" would wrongly read the main verb "pass" as a noun ("hard"
408 // there is an adverb). The lexicon's is_adverb misses "hard", so the
409 // article is the reliable signal.
410 let next_is_content = if self.current + 1 < self.tokens.len() {
411 let next = &self.tokens[self.current + 1].kind;
412 matches!(
413 next,
414 TokenType::Noun(_) | TokenType::Adjective(_) | TokenType::ProperName(_)
415 ) || ((definiteness.is_some() || self.nominal_np_context)
416 && matches!(next, TokenType::Verb { .. } | TokenType::Ambiguous { .. }))
417 } else {
418 false
419 };
420
421 if next_is_content {
422 if let TokenType::Adjective(adj) = self.advance().kind {
423 adjectives.push(adj);
424 }
425 } else {
426 break;
427 }
428 }
429
430 // "the 8:15 pm event" / "the 9:30am outing" — a clock time names the head
431 // noun; compound the minutes-from-midnight into the symbol so the entity is
432 // identified by its time ("1215_event"), distinct from "570_outing".
433 let base_noun = if let TokenType::TimeLiteral { nanos_from_midnight } = self.peek().kind {
434 let minutes = nanos_from_midnight / 60_000_000_000;
435 let next_is_head = matches!(
436 self.tokens.get(self.current + 1).map(|t| &t.kind),
437 Some(TokenType::Noun(_)) | Some(TokenType::ProperName(_))
438 | Some(TokenType::Adjective(_)) | Some(TokenType::Verb { .. })
439 | Some(TokenType::Ambiguous { .. })
440 );
441 if next_is_head {
442 self.advance(); // consume the time literal
443 let head = self.consume_content_word()?;
444 let head_str = self.interner.resolve(head).to_string();
445 self.interner.intern(&format!("{}_{}", minutes, head_str))
446 } else {
447 self.consume_content_word()?
448 }
449 }
450 // "the 1848 home" / "the 1834 flood" — cardinal used as a year/label modifier
451 // before the head noun; compound the two into a single symbol.
452 else if let TokenType::Cardinal(n) = self.peek().kind {
453 // The head after a count/label cardinal can tokenize as a noun, a
454 // proper name, an adjective, or — for words that are also verbs
455 // ("dances", "places") — a verb / ambiguous token.
456 let next_is_content = self.tokens.get(self.current + 1)
457 .map_or(false, |t| matches!(
458 t.kind,
459 TokenType::Noun(_)
460 | TokenType::ProperName(_)
461 | TokenType::Adjective(_)
462 | TokenType::Verb { .. }
463 | TokenType::Ambiguous { .. }
464 // "item"/"items" are nouns in declarative NL ("the $275
465 // item"); the lexer keeps them as keyword tokens.
466 | TokenType::Item
467 | TokenType::Items
468 ));
469 if next_is_content {
470 self.advance(); // consume the cardinal
471 let head = self.consume_label_head_noun_first()?;
472 self.numeric_label_head(n as i64, head, definiteness, &mut measure_restrictors)
473 } else if n == 1 {
474 // "the one who paid $150" / "the one with 804 followers" / "the one
475 // from St. Paul" — "one" used PRONOMINALLY (no following head noun)
476 // is the impersonal pronoun, a generic entity, not the numeral 1.
477 self.advance(); // consume "one"
478 self.interner.intern("One")
479 } else {
480 self.consume_content_word()?
481 }
482 }
483 // "the 2003 holiday" / "the 1850 stamp" — a YEAR/numeric label lexes as a
484 // bare Number (digits), not a word-Cardinal, but plays the same role: it
485 // names the head noun. Route it through the SAME label logic so a declared
486 // item un-fuses to its category relation and an undeclared one stays fused.
487 else if let TokenType::Number(num_sym) = self.peek().kind {
488 let next_is_content = self.tokens.get(self.current + 1).map_or(false, |t| {
489 matches!(
490 t.kind,
491 TokenType::Noun(_)
492 | TokenType::ProperName(_)
493 | TokenType::Adjective(_)
494 | TokenType::Verb { .. }
495 | TokenType::Ambiguous { .. }
496 | TokenType::Item
497 | TokenType::Items
498 )
499 });
500 // Only an INTEGER label (a year / instance number) compounds with the
501 // head; a unit measure ("3.5 inch") is left to the measure path, and a
502 // non-integer Number never names an instance.
503 let int_value = self
504 .interner
505 .resolve(num_sym)
506 .replace(',', "")
507 .parse::<i64>()
508 .ok();
509 match (next_is_content, int_value) {
510 (true, Some(n)) => {
511 self.advance(); // consume the number
512 let head = self.consume_label_head_noun_first()?;
513 self.numeric_label_head(n, head, definiteness, &mut measure_restrictors)
514 }
515 _ => self.consume_content_word()?,
516 }
517 } else {
518 self.consume_content_word()?
519 };
520
521 let noun = if let Some(prefix) = non_intersective_prefix {
522 let prefix_str = self.interner.resolve(prefix);
523 let base_str = self.interner.resolve(base_noun);
524 let compound = format!("{}-{}", prefix_str, base_str);
525 self.interner.intern(&compound)
526 } else {
527 base_noun
528 };
529
530 // Absorb EVERY consecutive proper-name label into the head ("Delta Gamma Pi" →
531 // Delta_Gamma_Pi, "Beta Pi Omega" → Beta_Pi_Omega) — a multi-word name can be
532 // 3+ words, not just two.
533 // …but in a DETERMINER-headed NP ("the prize Tara won", "the waterfall
534 // Derrick photographed") a proper name that opens an object-gap reduced
535 // relative is the relative's SUBJECT, NOT a label compounded into the head —
536 // leave it for the reduced-relative detection below. A bare multi-word proper
537 // name ("Ray Ricardo won") has no determiner, and an apposition with an overt
538 // object ("the dancer Tara won the prize") has no gap, so both still compound.
539 let mut noun = noun;
540 while self.check_proper_name_or_label()
541 && !(definiteness.is_some() && self.peek_reduced_object_relative())
542 {
543 let label = self.consume_content_word()?;
544 noun = self.interner.intern(&format!(
545 "{}_{}",
546 self.interner.resolve(noun),
547 self.interner.resolve(label)
548 ));
549 }
550 let noun = noun;
551
552 // US "City, ST" address — a place proper name followed by ", " + a
553 // two-letter ALL-CAPS state abbreviation is ONE location entity
554 // ("Charlestown, CT" → Charlestown_CT, "Barnstable, ME" → Barnstable_ME).
555 // The all-caps gate excludes Title-case names ("Al", "Bo") and ordinary
556 // list/clause commas (names are never two-letter all-caps), so this never
557 // swallows a coordinator.
558 let noun = if self.check(&TokenType::Comma) {
559 let state_abbr = matches!(
560 self.tokens.get(self.current + 1).map(|t| &t.kind),
561 Some(TokenType::ProperName(s))
562 if { let l = self.interner.resolve(*s); l.len() == 2 && l.chars().all(|c| c.is_ascii_uppercase()) }
563 );
564 let head_is_proper = self
565 .interner
566 .resolve(noun)
567 .chars()
568 .next()
569 .map_or(false, |c| c.is_ascii_uppercase());
570 if state_abbr && head_is_proper {
571 self.advance(); // consume the comma
572 let st = self.consume_content_word()?;
573 let compound = format!("{}_{}", self.interner.resolve(noun), self.interner.resolve(st));
574 self.interner.intern(&compound)
575 } else {
576 noun
577 }
578 } else {
579 noun
580 };
581
582 // Noun-noun compounds ("stop bit", "data bits", "grant signal"): a
583 // PLAIN noun directly after the head joins it as one compound head.
584 // An Ambiguous noun/verb word ("signal") joins ONLY when a copula
585 // follows it — there the verb reading is impossible — so genuine
586 // ambiguity ("Time flies like an arrow") keeps its verb reading and
587 // the forest enumerates the rest.
588 let mut noun = noun;
589 loop {
590 // The function word "as" lexes as a Noun (unknown-word fallback) but
591 // is never a compound-noun part — it introduces a predicative
592 // complement ("has Al Acosta AS its mayor"). Stop the compound here so
593 // the "as"-phrase is left for the verb path, not bundled into the head.
594 if matches!(self.peek().kind, TokenType::Noun(_))
595 && self.interner.resolve(self.peek().lexeme).eq_ignore_ascii_case("as")
596 {
597 break;
598 }
599 let next = match &self.peek().kind {
600 TokenType::Noun(next) => *next,
601 TokenType::Ambiguous { primary, alternatives } => {
602 let noun_reading = if let TokenType::Noun(n) = &**primary {
603 Some(*n)
604 } else {
605 alternatives.iter().find_map(|t| {
606 if let TokenType::Noun(n) = t { Some(*n) } else { None }
607 })
608 };
609 let copula_after = matches!(
610 self.tokens.get(self.current + 1).map(|t| &t.kind),
611 Some(TokenType::Is)
612 | Some(TokenType::Are)
613 | Some(TokenType::Was)
614 | Some(TokenType::Were)
615 );
616 // Inside an OBJECT/complement NP (greedy == false the clause
617 // already has its main verb) an ambiguous noun/verb word that
618 // is NOT itself followed by its own argument cannot be a verb —
619 // it joins the head as a compound ("has a glass head.", "holds
620 // a paper clip.", "played second base WON"). The following
621 // token confirms this: a clause boundary (Period/EOF/comma/
622 // and/or) or a FINITE verb/copula (the matrix or next clause's
623 // verb — "second base" then matrix "won"/"is") both rule out a
624 // verb reading of the ambiguous word. A clause subject (greedy
625 // == true) keeps the verb reading so "The man runs." is not
626 // eaten; wh-extraction is excluded (the gap leaves the embedded
627 // verb at a boundary, "…Mary said Bill saw?").
628 let object_compound_boundary = !greedy
629 && self.filler_gap.is_none()
630 && matches!(
631 self.tokens.get(self.current + 1).map(|t| &t.kind),
632 None | Some(TokenType::Period)
633 | Some(TokenType::EOF)
634 | Some(TokenType::Comma)
635 | Some(TokenType::And)
636 | Some(TokenType::Or)
637 | Some(TokenType::Verb { .. })
638 | Some(TokenType::Is)
639 | Some(TokenType::Are)
640 | Some(TokenType::Was)
641 | Some(TokenType::Were)
642 );
643 // A nominal context (copula complement / PP object /
644 // comparative standard) likewise rules out the verb reading:
645 // "is the Russell Road PROJECT", "the $5.25 PURCHASE".
646 // After a numeric-MEASURE head ("60_gallon …"), the following
647 // words are the measured head COMPOUND ("60 gallon FISH TANK"), so
648 // an ambiguous noun/verb word ("fish") joins the head even in a
649 // greedy subject — a measure premodifier cannot be followed by a
650 // finite verb, so the ambiguity resolves to noun.
651 let head_is_numeric_measure = self
652 .interner
653 .resolve(noun)
654 .chars()
655 .next()
656 .map_or(false, |c| c.is_ascii_digit());
657 match (
658 noun_reading,
659 copula_after
660 || object_compound_boundary
661 || self.nominal_np_context
662 || head_is_numeric_measure,
663 ) {
664 (Some(n), true) => n,
665 _ => break,
666 }
667 }
668 // A verb-only word ("stamp", "print") after a numeric/label head
669 // is a compound-noun part ("the 125000 stamp"). A bare number
670 // cannot head an NP that acts, so a numeric head takes the
671 // following verb-word as its real head noun whatever follows —
672 // BUT only in BASE form (surface == lemma): "stamp"/"print" read
673 // as nouns, while an inflected form ("in 1850 sold …", "runs") is
674 // a genuine verb and must not be eaten. Otherwise we require a
675 // copula after so "the man runs fast" keeps "runs" as the verb.
676 TokenType::Verb { lemma, .. } => {
677 let head_is_numeric = {
678 // Accept decimal/grouped money amounts as numeric heads
679 // ("$5.25 purchase" → 5.25, "$1,800 stamp" → 1,800) so the
680 // deverbal-noun fold fires — a bare number can't head an
681 // acting NP, so the following verb-word is its real head.
682 let h = self.interner.resolve(noun);
683 !h.is_empty()
684 && h.chars().any(|c| c.is_ascii_digit())
685 && h.chars().all(|c| c.is_ascii_digit() || c == '.' || c == ',')
686 };
687 let is_base_form = self
688 .interner
689 .resolve(self.peek().lexeme)
690 .eq_ignore_ascii_case(self.interner.resolve(*lemma));
691 let copula_after = matches!(
692 self.tokens.get(self.current + 1).map(|t| &t.kind),
693 Some(TokenType::Is)
694 | Some(TokenType::Are)
695 | Some(TokenType::Was)
696 | Some(TokenType::Were)
697 );
698 // A base-form verb-word is a deverbal noun ONLY at the NP TAIL —
699 // a clause boundary / finite verb / copula follows. If a VP
700 // continuation follows instead (adverb, object, PP), it is the
701 // MATRIX verb and must NOT be eaten: "the goods from Spain SELL
702 // quickly" (a PP-object/nominal context, base "sell" + adverb)
703 // keeps "sell" as the verb. This is the same NP-tail test the
704 // object-boundary case below uses.
705 let next_ends_np = matches!(
706 self.tokens.get(self.current + 1).map(|t| &t.kind),
707 None | Some(TokenType::Period)
708 | Some(TokenType::EOF)
709 | Some(TokenType::Comma)
710 | Some(TokenType::And)
711 | Some(TokenType::Or)
712 | Some(TokenType::Verb { .. })
713 | Some(TokenType::Is)
714 | Some(TokenType::Are)
715 | Some(TokenType::Was)
716 | Some(TokenType::Were)
717 );
718 // A BASE-FORM verb-word after a noun head, inside a NOMINAL
719 // context (a PP object / comparative standard) and at the NP tail,
720 // is a deverbal noun-noun COMPOUND: "a cork COVER", "an amber BASE",
721 // "a tataki ROLL". The base-form gate excludes inflected matrix
722 // verbs (runs/votes/sold); the context gate keeps SUBJECT NPs out.
723 let nominal_compound = self.nominal_np_context && is_base_form && next_ends_np;
724 // A CAPITALIZED verb-word after an already MULTI-WORD proper-name
725 // head ("Bald_Hill" + "Run") is the tail of a place name, not a
726 // verb. The head MUST already be compounded (contains '_') — this
727 // is what separates a place name from a SUBJECT + matrix verb
728 // ("John" + idiom lemma "Die" → must stay John kicked-the-bucket,
729 // not John_Die). Both sides capitalized + a clause boundary after.
730 let proper_name_part = self.interner.resolve(noun).contains('_')
731 && self.interner.resolve(noun).chars().next()
732 .map_or(false, |c| c.is_ascii_uppercase())
733 && self.interner.resolve(self.peek().lexeme).chars().next()
734 .map_or(false, |c| c.is_ascii_uppercase())
735 && matches!(
736 self.tokens.get(self.current + 1).map(|t| &t.kind),
737 None | Some(TokenType::Period) | Some(TokenType::EOF)
738 | Some(TokenType::Comma) | Some(TokenType::And)
739 | Some(TokenType::Or)
740 );
741 // A BASE-FORM verb-word in an OBJECT/complement NP (!greedy:
742 // the clause already has its verb), NOT followed by its own
743 // argument, is a deverbal noun joining the head ("the spicy
744 // tataki ROLL", "the onion DIP"). The following token confirms
745 // it: a clause boundary, or a finite verb/copula (the matrix or
746 // next clause's verb) — both rule out a verb reading. A PRONOUN
747 // after it ("own a donkey BEAT it") is NOT a boundary, so the
748 // nuclear verb of a quantifier is left alone. Mirrors the
749 // Ambiguous arm's object_compound_boundary.
750 let object_boundary =
751 !greedy && self.filler_gap.is_none() && is_base_form && next_ends_np;
752 // A clause-final GERUND (-ing) after a noun head in an object /
753 // nominal NP is a noun-incorporation compound ("started WEIGHT
754 // LIFTING", "enjoys BIRD WATCHING") — the noun is the gerund's
755 // incorporated object. Requires the NP tail (next_ends_np) so a
756 // reduced relative with its own object ("the man lifting WEIGHTS")
757 // is untouched. Uses the SURFACE form so the compound is
758 // weight_lifting, not the lemma weight_lift.
759 let gerund_compound = (!greedy || self.nominal_np_context)
760 && self.filler_gap.is_none()
761 && self
762 .interner
763 .resolve(self.peek().lexeme)
764 .to_lowercase()
765 .ends_with("ing")
766 && next_ends_np;
767 if (head_is_numeric && is_base_form) || copula_after || nominal_compound || proper_name_part || object_boundary {
768 *lemma
769 } else if gerund_compound {
770 self.peek().lexeme
771 } else {
772 break;
773 }
774 }
775 _ => break,
776 };
777 self.advance();
778 let head_str = self.interner.resolve(noun);
779 let next_str = self.interner.resolve(next);
780 let compound = format!("{}_{}", head_str, next_str);
781 noun = self.interner.intern(&compound);
782 }
783
784 // Head noun + numeric value: an instance/slot LABEL — "number 7",
785 // "room 204", "lane 3", "version 2", "exhibit 5", "car 7". This is a
786 // GENERAL grammatical pattern, not a hardcoded word list (which would
787 // never generalise to unseen clues): in English a common noun is
788 // otherwise never directly followed by a BARE number, so a head noun
789 // immediately followed by a Cardinal/Number names a specific instance —
790 // join them into one symbol. Both word-numbers ("seven") and digits ("7")
791 // apply. The one exception is a MEASURE ("a box 3 FEET tall"), where the
792 // number begins a unit phrase — a following CalendarUnit / registered unit
793 // word vetoes the label reading. Gated to declarative (NL) parsing so it
794 // never disturbs LOGOS imperative index syntax ("item 3 of arr").
795 let label_value: Option<String> = match self.peek().kind {
796 TokenType::Cardinal(n) => Some(n.to_string()),
797 TokenType::Number(sym) => Some(self.interner.resolve(sym).to_string()),
798 _ => None,
799 };
800 if let Some(value) = label_value {
801 let number_starts_measure = match self.tokens.get(self.current + 1) {
802 Some(t) => matches!(t.kind, TokenType::CalendarUnit(_))
803 || crate::lexicon::lookup_unit_dimension(
804 &self.interner.resolve(t.lexeme).to_lowercase(),
805 )
806 .is_some(),
807 None => false,
808 };
809 let head_str = self.interner.resolve(noun).to_string();
810 // A numeric head ("1850") is not a label base; only a real word is.
811 let head_is_word = !head_str.is_empty() && !head_str.chars().all(|c| c.is_ascii_digit());
812 // A MONTH head ("April 15") is a date, handled by the month+day rule
813 // just below — never an instance label.
814 let head_is_month = crate::lexicon::is_month(&head_str.to_lowercase());
815 if !number_starts_measure
816 && head_is_word
817 && !head_is_month
818 && self.mode == super::ParserMode::Declarative
819 {
820 self.advance();
821 noun = self.interner.intern(&format!("{}_{}", head_str, value));
822 }
823 }
824
825 // Month + day: "June 11", "May 3", "December 25" — the day (1–31) joins the
826 // month into a single date symbol ("June_11"). Month names are a closed
827 // lexical class (lexicon `months`).
828 {
829 let head_lower = self.interner.resolve(noun).to_lowercase();
830 if crate::lexicon::is_month(&head_lower) {
831 let day = match self.peek().kind {
832 TokenType::Cardinal(n) if (1..=31).contains(&n) => Some(n),
833 // A numeric day may carry an ordinal suffix ("15th", "3rd",
834 // "1st", "2nd"); strip it so "April 15th" and "April 15" name
835 // the same date ("April_15").
836 TokenType::Number(s) => {
837 let raw = self.interner.resolve(s);
838 let digits = raw.trim_end_matches(|c: char| c.is_ascii_alphabetic());
839 digits.parse::<u32>().ok().filter(|d| (1..=31).contains(d))
840 }
841 _ => None,
842 };
843 if let Some(day) = day {
844 self.advance();
845 let head_str = self.interner.resolve(noun);
846 noun = self.interner.intern(&format!("{}_{}", head_str, day));
847 // An attributive date modifies a following head noun ("the
848 // April 15th birthday" → April_15_birthday); absorb it so the
849 // real head isn't stranded.
850 while let TokenType::Noun(next) = self.peek().kind {
851 self.advance();
852 noun = self.interner.intern(&format!(
853 "{}_{}",
854 self.interner.resolve(noun),
855 self.interner.resolve(next)
856 ));
857 }
858 }
859 }
860 }
861
862 if self.check_possessive() {
863 self.advance();
864
865 let possessor = self.ctx.nps.alloc(NounPhrase {
866 definiteness,
867 adjectives: self.ctx.syms.alloc_slice(adjectives.clone()),
868 noun,
869 possessor: None,
870 pps: &[],
871 superlative: superlative_adj,
872 });
873
874 let mut possessed_noun = self.consume_content_word()?;
875 // A multi-word possessed compound noun ("Bernard's fountain pen",
876 // "Joe's yoga session") joins its consecutive nouns into one symbol,
877 // mirroring the head noun-noun compounding above.
878 while let TokenType::Noun(next) = self.peek().kind {
879 self.advance();
880 possessed_noun = self.interner.intern(&format!(
881 "{}_{}",
882 self.interner.resolve(possessed_noun),
883 self.interner.resolve(next)
884 ));
885 }
886
887 return Ok(NounPhrase {
888 definiteness: None,
889 adjectives: &[],
890 noun: possessed_noun,
891 possessor: Some(possessor),
892 pps: &[],
893 superlative: None,
894 });
895 }
896
897 let should_attach_pps = greedy || self.pp_attach_to_noun;
898
899 let mut pps: Vec<&'a LogicExpr<'a>> = Vec::new();
900 // Attributive measure-adjectives ("80 year old") are core restrictors of the
901 // head — collected before it — so they attach unconditionally, ahead of any
902 // optional trailing PPs.
903 pps.extend(measure_restrictors.iter().copied());
904
905 // An "of <measure>" restrictor ("a maximum range OF 475 ft", "a book OF
906 // 500 pages") names the head noun's measured value. It is UNAMBIGUOUSLY a
907 // noun restrictor (never an event adjunct or a partitive), so attach it
908 // even in a non-greedy object NP — otherwise "has a maximum range of 475
909 // ft" silently strands the measure.
910 loop {
911 let of_measure = self.check_of_preposition()
912 && matches!(
913 self.tokens.get(self.current + 1).map(|t| &t.kind),
914 Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
915 );
916 if !of_measure {
917 break;
918 }
919 let of_sym = match self.advance().kind {
920 TokenType::Preposition(s) => s,
921 _ => break,
922 };
923 let placeholder_var = self.interner.intern("_PP_SELF_");
924 let measure = self.parse_measure_phrase()?;
925 pps.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
926 name: of_sym,
927 args: self
928 .ctx
929 .terms
930 .alloc_slice([Term::Variable(placeholder_var), *measure]),
931 world: None,
932 }));
933 }
934
935 // Postposed measure-adjective "worth <measure>" ("the magnate WORTH $27
936 // billion", "a stamp WORTH $50") — a postnominal adjective taking a measure
937 // complement, the same Worth(x, measure) the copula complement builds. Without
938 // this "worth" strands (TrailingTokens{Adjective}). Surface it as a restrictor
939 // over the _PP_SELF_ placeholder so it lowers onto the NP's entity.
940 if matches!(self.peek().kind, TokenType::Adjective(_))
941 && self
942 .interner
943 .resolve(self.peek().lexeme)
944 .eq_ignore_ascii_case("worth")
945 && matches!(
946 self.tokens.get(self.current + 1).map(|t| &t.kind),
947 Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
948 )
949 {
950 let worth_sym = if let TokenType::Adjective(s) = self.peek().kind {
951 s
952 } else {
953 unreachable!()
954 };
955 self.advance(); // "worth"
956 let measure = self.parse_measure_phrase()?;
957 let placeholder = self.interner.intern("_PP_SELF_");
958 // Push directly to `pps` — the measure_restrictors → pps merge already ran
959 // above (this is the post-head section).
960 pps.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
961 name: worth_sym,
962 args: self
963 .ctx
964 .terms
965 .alloc_slice([Term::Variable(placeholder), *measure]),
966 world: None,
967 }));
968 }
969
970 if should_attach_pps {
971 // "of" normally starts a partitive/genitive handled elsewhere, so it
972 // is excluded here — EXCEPT "of <measure>" ("a range of 650 ft", "a
973 // book of 500 pages"), where the of-phrase specifies the head noun's
974 // measured value. That is a genuine restrictor, not a partitive, so
975 // attach it as an Of(self, <measure>) PP.
976 let of_measure_follows = |p: &Self| {
977 p.check_of_preposition()
978 && matches!(
979 p.tokens.get(p.current + 1).map(|t| &t.kind),
980 Some(TokenType::Number(_)) | Some(TokenType::Cardinal(_))
981 )
982 };
983 while self.check_preposition() && (!self.check_of_preposition() || of_measure_follows(self)) {
984 let prep_token = self.advance().clone();
985 let prep_name = if let TokenType::Preposition(sym) = prep_token.kind {
986 sym
987 } else {
988 break;
989 };
990
991 let placeholder_var = self.interner.intern("_PP_SELF_");
992 if self.check_number()
993 && !matches!(
994 self.tokens.get(self.current + 2).map(|t| &t.kind),
995 Some(TokenType::Noun(_)) | Some(TokenType::Ambiguous { .. })
996 )
997 {
998 // Numeric PP object ("with 15 people", "at 385 degrees"):
999 // keep the measured amount as the PP's object. A noun/ambiguous
1000 // head after the unit ("with 205 degree WATER") is instead a
1001 // measure-premodified noun, folded by the NP branch below.
1002 let measure = self.parse_measure_phrase()?;
1003 let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1004 name: prep_name,
1005 args: self
1006 .ctx
1007 .terms
1008 .alloc_slice([Term::Variable(placeholder_var), *measure]),
1009 world: None,
1010 });
1011 pps.push(pp_pred);
1012 } else if self.check_content_word()
1013 || self.check_number()
1014 || matches!(self.peek().kind, TokenType::Article(_))
1015 {
1016 // A PP object is an unambiguously NOMINAL tail, so a base-form
1017 // verb-word after its noun head is a deverbal compound ("with a
1018 // cork COVER", "with a faux leather COVER"), not a matrix verb.
1019 let saved_ctx = self.nominal_np_context;
1020 self.nominal_np_context = true;
1021 let pp_object_result = self.parse_noun_phrase(true);
1022 self.nominal_np_context = saved_ctx;
1023 let pp_object = pp_object_result?;
1024 let pp_pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1025 name: prep_name,
1026 args: self.ctx.terms.alloc_slice([
1027 Term::Variable(placeholder_var),
1028 Term::Constant(pp_object.noun),
1029 ]),
1030 world: None,
1031 });
1032 pps.push(pp_pred);
1033 // The PP object's own ADJECTIVES ("with the BLUE hat" →
1034 // With(self, Hat) ∧ Blue(Hat)) and nested restrictors ("with a
1035 // maximum range of 650 ft" → … ∧ Of(Range, 650 ft)) survive via
1036 // the shared recovery (the single source of truth for every PP
1037 // position); dropping them would be meaning loss.
1038 pps.extend(self.pp_object_modifier_preds(&pp_object));
1039 } else {
1040 break;
1041 }
1042 }
1043
1044 // Active object-gap reduced relative ("the waterfall Derrick photographed
1045 // in 1989" = the waterfall [that] Derrick photographed): a determiner-
1046 // headed NP whose head is followed by a reduced-relative subject+verb with
1047 // an EMPTY object slot (the gap is THIS head; English drops the
1048 // relativizer). The empty-object + transitive test (peek_reduced_object_relative)
1049 // is the PROPER deterministic discriminator — an overt object is apposition,
1050 // an intransitive verb has no gap. Reuse parse_relative_clause with the
1051 // `_PP_SELF_` placeholder so the clause — and its event complements ("in
1052 // 1989") — attach as a restrictor wherever the NP flows.
1053 if definiteness.is_some() && self.peek_reduced_object_relative() {
1054 let placeholder = self.interner.intern("_PP_SELF_");
1055 let rel = self.parse_relative_clause(placeholder)?;
1056 pps.push(rel);
1057 }
1058
1059 // Post-nominal "-ing" reduced relative ("the person arriving at
1060 // Paradise", "the assignment beginning in June", "the conductor
1061 // working on June 11"): a present participle after the noun is
1062 // unambiguously a reduced relative — it cannot be a finite main verb
1063 // without an auxiliary — so it restricts THIS noun phrase wherever the
1064 // NP appears (subject, standard, of-pair member, predicate nominal).
1065 // Attach as predicates over the `_PP_SELF_` placeholder (substituted
1066 // to the NP's variable when the NP is wrapped).
1067 if let TokenType::Verb { lemma, .. } = self.peek().kind {
1068 let is_ing = self
1069 .interner
1070 .resolve(self.peek().lexeme)
1071 .to_lowercase()
1072 .ends_with("ing");
1073 // A past participle followed by a "by"-agent is an unambiguous
1074 // PASSIVE reduced relative ("the bird trained BY the falconer",
1075 // "the photo published BY Wildzone") — a finite main verb is not
1076 // followed by a by-phrase in this position.
1077 let passive_by = matches!(
1078 self.tokens.get(self.current + 1).map(|t| &t.kind),
1079 Some(TokenType::Preposition(s))
1080 if self.interner.resolve(*s).eq_ignore_ascii_case("by")
1081 );
1082 // A past participle of a TRANSITIVE verb immediately followed by a
1083 // PP (no object) is a passive reduced relative ("the medicine
1084 // sourced FROM a fig", "the item made OF teak", "the flower grown
1085 // IN Olin"). Lexical transitivity disambiguates it from an
1086 // intransitive main clause ("the box arrived in April"), which a
1087 // bare common noun + PP would otherwise look like.
1088 //
1089 // The near-dead `is_transitive_verb` table (47/2623 verbs) misses
1090 // most transitives ("the gator CAUGHT in Lynn" — catch has past ==
1091 // participle, so the distinct-form rule can't save it either). In a
1092 // NOMINAL complement position (`nominal_np_context`: "X is the gator
1093 // caught in Lynn") the NP cannot be a main-clause subject, so a
1094 // past-participle + PP IS a reduced relative for any verb that is not
1095 // marked pure-intransitive — the same transitive-capable-by-default
1096 // rule the object-gap relative uses. In subject position the strict
1097 // table still gates it (keeping "The team won in 1989" a main clause).
1098 let transitive_passive = matches!(
1099 self.peek().kind,
1100 TokenType::Verb { time: crate::lexicon::Time::Past, .. }
1101 ) && matches!(
1102 self.tokens.get(self.current + 1).map(|t| &t.kind),
1103 Some(TokenType::Preposition(_))
1104 ) && (crate::lexicon::is_transitive_verb(
1105 &self.interner.resolve(lemma).to_lowercase(),
1106 ) || (self.nominal_np_context
1107 && !crate::lexicon::is_intransitive_verb(
1108 &self.interner.resolve(lemma).to_lowercase(),
1109 )));
1110 // A DISTINCT past-participle form (participle ≠ past: "grown" vs
1111 // "grew", "taken" vs "took") immediately followed by a PP is an
1112 // unambiguous passive reduced relative regardless of transitivity —
1113 // the form alone is non-finite, like "-ing" ("the flower GROWN in
1114 // Hardy", "the package TAKEN to the depot").
1115 let distinct_participle_passive = matches!(
1116 self.tokens.get(self.current + 1).map(|t| &t.kind),
1117 Some(TokenType::Preposition(_))
1118 ) && crate::lexicon::is_distinct_past_participle(
1119 &self.interner.resolve(self.peek().lexeme).to_lowercase(),
1120 );
1121 if is_ing || passive_by || transitive_passive || distinct_participle_passive {
1122 self.advance();
1123 let placeholder = self.interner.intern("_PP_SELF_");
1124 // An ACTIVE -ing reduced relative can take a DIRECT OBJECT
1125 // ("the origami DEPICTING a dragon", "the survivor BRINGING the
1126 // rope") → Participle(x, object). Only -ing (a passive
1127 // participle's patient is the head itself), and only a real
1128 // object NP (article / content word), never a preposition or
1129 // the matrix verb.
1130 let direct_obj = if is_ing
1131 && (matches!(self.peek().kind, TokenType::Article(_))
1132 || self.check_content_word())
1133 && !self.check_preposition()
1134 && !self.check_verb()
1135 {
1136 Some(self.parse_noun_phrase(true)?)
1137 } else {
1138 None
1139 };
1140 let participle_pred = if let Some(ref obj) = direct_obj {
1141 self.ctx.exprs.alloc(LogicExpr::Predicate {
1142 name: lemma,
1143 args: self.ctx.terms.alloc_slice([
1144 Term::Variable(placeholder),
1145 Term::Constant(obj.noun),
1146 ]),
1147 world: None,
1148 })
1149 } else {
1150 self.ctx.exprs.alloc(LogicExpr::Predicate {
1151 name: lemma,
1152 args: self.ctx.terms.alloc_slice([Term::Variable(placeholder)]),
1153 world: None,
1154 })
1155 };
1156 pps.push(participle_pred);
1157 // The participle's PP / directional-"to" complements. "of" is
1158 // allowed here ("made OF teak", "made OF sandalwood") — it is
1159 // the participle's material complement, not a possessive.
1160 loop {
1161 let prep = if self.check_preposition() {
1162 match self.advance().kind {
1163 TokenType::Preposition(s) => s,
1164 _ => break,
1165 }
1166 } else if self.check(&TokenType::To)
1167 && matches!(
1168 self.tokens.get(self.current + 1).map(|t| &t.kind),
1169 Some(TokenType::Article(_))
1170 | Some(TokenType::Noun(_))
1171 | Some(TokenType::ProperName(_))
1172 )
1173 {
1174 self.advance();
1175 self.interner.intern("To")
1176 } else {
1177 break;
1178 };
1179 if self.check_number() {
1180 // A numeric PP object in a reduced relative: "found IN
1181 // 1992", "sent in 1976", "donated in 2010" — keep the
1182 // year/amount, else it strands.
1183 let measure = self.parse_measure_phrase()?;
1184 pps.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
1185 name: prep,
1186 args: self.ctx.terms.alloc_slice([
1187 Term::Variable(placeholder),
1188 *measure,
1189 ]),
1190 world: None,
1191 }));
1192 } else if self.check_content_word()
1193 || matches!(self.peek().kind, TokenType::Article(_))
1194 {
1195 let obj = self.parse_noun_phrase(true)?;
1196 pps.push(self.ctx.exprs.alloc(LogicExpr::Predicate {
1197 name: prep,
1198 args: self.ctx.terms.alloc_slice([
1199 Term::Variable(placeholder),
1200 Term::Constant(obj.noun),
1201 ]),
1202 world: None,
1203 }));
1204 } else {
1205 break;
1206 }
1207 }
1208 }
1209 }
1210 }
1211 let pps_slice = self.ctx.pps.alloc_slice(pps);
1212
1213 if self.check_of_preposition() {
1214 // Two-Pass Type Disambiguation:
1215 // If the noun is a known generic type (e.g., "Stack", "List"),
1216 // then "X of Y" is a type instantiation, not a possessive.
1217 // For now, we still parse it as possessive structurally, but
1218 // the type_registry enables future AST extensions for type annotations.
1219 let is_generic = self.is_generic_type(noun);
1220
1221 if !is_generic {
1222 // Standard possessive: "owner of house" → possessor relationship
1223 self.advance();
1224
1225 let possessor_np = self.parse_noun_phrase(true)?;
1226 let possessor = self.ctx.nps.alloc(possessor_np);
1227
1228 return Ok(NounPhrase {
1229 definiteness,
1230 adjectives: self.ctx.syms.alloc_slice(adjectives),
1231 noun,
1232 possessor: Some(possessor),
1233 pps: pps_slice,
1234 superlative: superlative_adj,
1235 });
1236 }
1237 // If generic type, fall through to regular noun phrase handling.
1238 // The "of [Type]" will be left unparsed for now.
1239 // Future: Parse as GenericType { base: noun, params: [...] }
1240 }
1241
1242 // Register ALL noun phrases as discourse entities, not just definite ones.
1243 // This is needed for bridging anaphora: "I bought a car. The engine smoked."
1244 // The indefinite "a car" must be in discourse history for "the engine" to link to it.
1245 let noun_str = self.interner.resolve(noun);
1246 let first_char = noun_str.chars().next().unwrap_or('X');
1247 if first_char.is_alphabetic() {
1248 // Use full noun name as symbol for consistent output in Full mode
1249 let symbol = capitalize_first(noun_str);
1250 let number = if noun_str.ends_with('s') && !noun_str.ends_with("ss") {
1251 Number::Plural
1252 } else {
1253 Number::Singular
1254 };
1255 }
1256
1257 Ok(NounPhrase {
1258 definiteness,
1259 adjectives: self.ctx.syms.alloc_slice(adjectives),
1260 noun,
1261 possessor: possessor_from_pronoun,
1262 pps: pps_slice,
1263 superlative: superlative_adj,
1264 })
1265 }
1266
1267 fn parse_noun_phrase_for_relative(&mut self) -> ParseResult<NounPhrase<'a>> {
1268 let mut definiteness = None;
1269 let mut adjectives = Vec::new();
1270
1271 if let TokenType::Article(def) = self.peek().kind {
1272 definiteness = Some(def);
1273 self.advance();
1274 }
1275
1276 loop {
1277 if self.is_at_end() {
1278 break;
1279 }
1280
1281 let is_adjective = matches!(self.peek().kind, TokenType::Adjective(_));
1282 if !is_adjective {
1283 break;
1284 }
1285
1286 let next_is_content = if self.current + 1 < self.tokens.len() {
1287 matches!(
1288 self.tokens[self.current + 1].kind,
1289 TokenType::Noun(_)
1290 | TokenType::Adjective(_)
1291 | TokenType::Verb { .. }
1292 | TokenType::ProperName(_)
1293 )
1294 } else {
1295 false
1296 };
1297
1298 if next_is_content {
1299 if let TokenType::Adjective(adj) = self.advance().kind.clone() {
1300 adjectives.push(adj);
1301 }
1302 } else {
1303 break;
1304 }
1305 }
1306
1307 let noun = self.consume_content_word_for_relative()?;
1308
1309 if self.check(&TokenType::That) || self.check(&TokenType::Who) {
1310 self.advance();
1311 let var_name = self.interner.intern(&format!("r{}", self.var_counter));
1312 self.var_counter += 1;
1313 let _nested_clause = self.parse_relative_clause(var_name)?;
1314 }
1315
1316 Ok(NounPhrase {
1317 definiteness,
1318 adjectives: self.ctx.syms.alloc_slice(adjectives),
1319 noun,
1320 possessor: None,
1321 pps: &[],
1322 superlative: None,
1323 })
1324 }
1325
1326 fn noun_phrase_to_term(&self, np: &NounPhrase<'a>) -> Term<'a> {
1327 if let Some(possessor) = np.possessor {
1328 let possessor_term = self.noun_phrase_to_term(possessor);
1329 Term::Possessed {
1330 possessor: self.ctx.terms.alloc(possessor_term),
1331 possessed: np.noun,
1332 }
1333 } else {
1334 Term::Constant(np.noun)
1335 }
1336 }
1337
1338 fn numeric_label_head(
1339 &mut self,
1340 n: i64,
1341 head: crate::intern::Symbol,
1342 definiteness: Option<Definiteness>,
1343 measure_restrictors: &mut Vec<&'a LogicExpr<'a>>,
1344 ) -> crate::intern::Symbol {
1345 // A numeric label that names a DECLARED item ("2003" registered as a
1346 // year) un-fuses: the label becomes the head noun PLUS a
1347 // P(_PP_SELF_, <number>) restrictor, converging with the
1348 // prepositional-phrase form ("the holiday was in 2003" →
1349 // Holiday(x) ∧ In(x, 2003)). The numeric term mirrors the PP form's
1350 // measure value EXACTLY so the two unify. Un-fusing is gated to definite
1351 // descriptions (a label refers); an undeclared number — or a category
1352 // that maps to no preposition — keeps the fused symbol.
1353 if definiteness == Some(Definiteness::Definite) {
1354 let item_sym = self.interner.intern(&n.to_string());
1355 if let Some(category) = self.drs.item_category(item_sym) {
1356 let cat_lemma = Self::singularize_noun(self.interner.resolve(category));
1357 if let Some(prep) = category_preposition(&cat_lemma.to_lowercase()) {
1358 let prep_sym = self.interner.intern(prep);
1359 let placeholder = self.interner.intern("_PP_SELF_");
1360 let value = Term::Value {
1361 kind: crate::ast::logic::NumberKind::Integer(n),
1362 unit: None,
1363 dimension: None,
1364 };
1365 let pred = self.ctx.exprs.alloc(LogicExpr::Predicate {
1366 name: prep_sym,
1367 args: self
1368 .ctx
1369 .terms
1370 .alloc_slice([Term::Variable(placeholder), value]),
1371 world: None,
1372 });
1373 measure_restrictors.push(pred);
1374 return head;
1375 }
1376 }
1377 }
1378 let head_str = self.interner.resolve(head);
1379 self.interner.intern(&format!("{}_{}", n, head_str))
1380 }
1381
1382 fn consume_label_head_noun_first(&mut self) -> ParseResult<crate::intern::Symbol> {
1383 let noun_sym = if let TokenType::Ambiguous { primary, alternatives } = &self.peek().kind {
1384 let from_primary = match **primary {
1385 TokenType::Noun(s) | TokenType::Adjective(s) => Some(s),
1386 _ => None,
1387 };
1388 from_primary.or_else(|| {
1389 alternatives.iter().find_map(|t| match t {
1390 TokenType::Noun(s) | TokenType::Adjective(s) => Some(*s),
1391 _ => None,
1392 })
1393 })
1394 } else {
1395 None
1396 };
1397 if let Some(s) = noun_sym {
1398 self.advance();
1399 return Ok(s);
1400 }
1401 self.consume_content_word()
1402 }
1403
1404 fn check_possessive(&self) -> bool {
1405 matches!(self.peek().kind, TokenType::Possessive)
1406 }
1407
1408 /// Whether the cursor is at the SUBJECT of an active object-gap reduced relative
1409 /// ("the prize | Tara won", cursor at "Tara"). The PROPER, deterministic test
1410 /// (no trial-parse): a proper-name / pronoun subject, then a TRANSITIVE verb
1411 /// (only a transitive verb has an object slot for the head to fill — an
1412 /// intransitive "the dancer Tara performed" is apposition, not a relative), then
1413 /// an EMPTY object slot — the token after the verb does NOT start a direct object
1414 /// (an overt object "the dancer Tara won THE PRIZE" is apposition, not a gap).
1415 /// The caller additionally requires a determiner-headed NP.
1416 fn peek_reduced_object_relative(&self) -> bool {
1417 if !matches!(self.peek().kind, TokenType::ProperName(_) | TokenType::Pronoun { .. }) {
1418 return false;
1419 }
1420 // A token that would START a direct/prepositional object — its presence after
1421 // the verb means the slot is filled (apposition), so there is no gap.
1422 let starts_object = |kind: Option<&TokenType>| {
1423 matches!(
1424 kind,
1425 Some(TokenType::Article(_))
1426 | Some(TokenType::Noun(_))
1427 | Some(TokenType::ProperName(_))
1428 | Some(TokenType::Number(_))
1429 | Some(TokenType::Cardinal(_))
1430 | Some(TokenType::Possessive)
1431 | Some(TokenType::Pronoun { .. })
1432 | Some(TokenType::All)
1433 | Some(TokenType::Some)
1434 | Some(TokenType::No)
1435 | Some(TokenType::Any)
1436 | Some(TokenType::Most)
1437 | Some(TokenType::Few)
1438 | Some(TokenType::Many)
1439 )
1440 };
1441 // Assume transitive-CAPABLE by default (English verbs overwhelmingly are);
1442 // only a verb marked pure-intransitive has no DIRECT object slot to fill.
1443 let verb_is_intransitive = self.tokens.get(self.current + 1).map_or(true, |t| {
1444 matches!(t.kind, TokenType::Verb { lemma, .. }
1445 if crate::lexicon::is_intransitive_verb(
1446 &self.interner.resolve(lemma).to_lowercase()))
1447 });
1448 let verb_follows = matches!(
1449 self.tokens.get(self.current + 1).map(|t| &t.kind),
1450 Some(TokenType::Verb { .. })
1451 );
1452 if !verb_follows {
1453 return false;
1454 }
1455 // A STRANDED preposition after the verb ("the friend Simon went WITH", "the
1456 // animal Eva works WITH") makes the GAP the object of that preposition, so
1457 // even a pure-intransitive verb heads the relative. The preposition's own
1458 // object slot must be empty (the next token does not start an NP).
1459 let stranded_prep = matches!(
1460 self.tokens.get(self.current + 2).map(|t| &t.kind),
1461 Some(TokenType::Preposition(_))
1462 ) && !starts_object(self.tokens.get(self.current + 3).map(|t| &t.kind));
1463 if stranded_prep {
1464 return true;
1465 }
1466 // Otherwise a transitive verb with an EMPTY direct-object slot (the head is
1467 // the gap); an overt object after the verb is apposition, not a gap.
1468 if verb_is_intransitive {
1469 return false;
1470 }
1471 !starts_object(self.tokens.get(self.current + 2).map(|t| &t.kind))
1472 }
1473
1474 fn peek_definite_reduced_relative_object(&self) -> bool {
1475 // The cursor must open a DEFINITE NP.
1476 if !matches!(self.peek().kind, TokenType::Article(Definiteness::Definite)) {
1477 return false;
1478 }
1479 // Skip the article, then any adjectives, to land on the noun head.
1480 let mut p = self.current + 1;
1481 while matches!(
1482 self.tokens.get(p).map(|t| &t.kind),
1483 Some(TokenType::Adjective(_)) | Some(TokenType::NonIntersectiveAdjective(_))
1484 ) {
1485 p += 1;
1486 }
1487 // The relativized head: a common noun.
1488 if !matches!(
1489 self.tokens.get(p).map(|t| &t.kind),
1490 Some(TokenType::Noun(_))
1491 | Some(TokenType::CalendarUnit(_))
1492 | Some(TokenType::Ambiguous { .. })
1493 ) {
1494 return false;
1495 }
1496 let subj = p + 1;
1497 // The relative's overt subject is a fresh ProperName / Pronoun.
1498 if !matches!(
1499 self.tokens.get(subj).map(|t| &t.kind),
1500 Some(TokenType::ProperName(_)) | Some(TokenType::Pronoun { .. })
1501 ) {
1502 return false;
1503 }
1504 // …immediately followed by the relative's finite verb.
1505 let vp = subj + 1;
1506 if !matches!(
1507 self.tokens.get(vp).map(|t| &t.kind),
1508 Some(TokenType::Verb { .. })
1509 ) {
1510 return false;
1511 }
1512 // Either a stranded preposition (gap = its object: "the friend Simon went
1513 // WITH") or a transitive verb with an empty direct-object slot (gap = the
1514 // head: "the waterfall Derrick photographed"). A filled slot after the verb
1515 // is apposition, not a gap.
1516 let starts_object = |kind: Option<&TokenType>| {
1517 matches!(
1518 kind,
1519 Some(TokenType::Article(_))
1520 | Some(TokenType::Noun(_))
1521 | Some(TokenType::ProperName(_))
1522 | Some(TokenType::Number(_))
1523 | Some(TokenType::Cardinal(_))
1524 | Some(TokenType::Possessive)
1525 | Some(TokenType::Pronoun { .. })
1526 | Some(TokenType::All)
1527 | Some(TokenType::Some)
1528 | Some(TokenType::No)
1529 | Some(TokenType::Any)
1530 | Some(TokenType::Most)
1531 | Some(TokenType::Few)
1532 | Some(TokenType::Many)
1533 )
1534 };
1535 let after_verb = self.tokens.get(vp + 1).map(|t| &t.kind);
1536 if matches!(after_verb, Some(TokenType::Preposition(_)))
1537 && !starts_object(self.tokens.get(vp + 2).map(|t| &t.kind))
1538 {
1539 return true;
1540 }
1541 let verb_is_intransitive = self.tokens.get(vp).map_or(true, |t| {
1542 matches!(t.kind, TokenType::Verb { lemma, .. }
1543 if crate::lexicon::is_intransitive_verb(
1544 &self.interner.resolve(lemma).to_lowercase()))
1545 });
1546 if verb_is_intransitive {
1547 return false;
1548 }
1549 !starts_object(after_verb)
1550 }
1551
1552 fn check_of_preposition(&self) -> bool {
1553 if let TokenType::Preposition(p) = self.peek().kind {
1554 p.is(self.interner, "of")
1555 } else {
1556 false
1557 }
1558 }
1559
1560 fn check_proper_name_or_label(&self) -> bool {
1561 match &self.peek().kind {
1562 TokenType::ProperName(_) => true,
1563 TokenType::Noun(s) => {
1564 let str_val = self.interner.resolve(*s);
1565 str_val.len() == 1 && str_val.chars().next().unwrap().is_uppercase()
1566 }
1567 _ => false,
1568 }
1569 }
1570
1571 fn check_possessive_pronoun(&self) -> bool {
1572 match &self.peek().kind {
1573 TokenType::Pronoun { case: Case::Possessive, .. } => true,
1574 TokenType::Ambiguous { primary, alternatives } => {
1575 let is_possessive = matches!(
1576 **primary,
1577 TokenType::Pronoun { case: Case::Possessive, .. }
1578 ) || alternatives.iter().any(|alt| {
1579 matches!(alt, TokenType::Pronoun { case: Case::Possessive, .. })
1580 });
1581 if !is_possessive {
1582 return false;
1583 }
1584 if self.noun_priority_mode {
1585 return true;
1586 }
1587 // Outside noun-priority contexts, the possessive reading of an
1588 // object/possessive-ambiguous pronoun ("her") applies exactly
1589 // when an NP head follows: "saw her dog" vs "saw her".
1590 self.possessive_np_head_follows()
1591 }
1592 _ => false,
1593 }
1594 }
1595}