Weksa/crates/weksa/src/lower.rs

508 lines
19 KiB
Rust

//! The English lowerer: the validated lowering view of a packet to text, by a
//! fixed table keyed by inventory ids. It renders predications, referents and the
//! speech act, and nothing else: no style, no model, no corpus text. It takes a
//! [`LoweringInput`], so it cannot see the trace or the corpus item. A packet
//! carrying a feature, relation, extension, role fill or referent it does not
//! render is refused, because rendering part of a meaning would assert a different
//! one (`polarity: negative` dropped says the opposite). Context, kind and
//! constraints steer delivery and are not rendered; the concept of a predication
//! must resolve but the frame owns the verb.
//!
//! A referent that cites an entity renders as that entity's own form, read
//! through the [`EntityForms`] port: the first (citation) form stored for
//! [`TARGET_LANGUAGE`], verbatim. The choice is a table lookup on the language
//! tag and the form's position, not a reading of the form. Forms after the
//! first are inflected or particle-bound shapes; no English frame asks for one,
//! so none is selected. An entity the port does not hold, or one with no form in
//! the target language, is refused: the concept word would say a different
//! thing than the name the packet cites.
//!
//! A request clause ("You asked Nibu to stop.") says what the addressee did, so
//! it is gated on the packet's own record of it: `Discourse.addressee_consent`
//! (set only by authoring or game telemetry, V15) must name that predication,
//! and its agent must be the addressee. Without it the lowerer refuses
//! ([`LowerError::ConsentMissing`], [`LowerError::AskerNotAddressee`]); it does
//! not read wording or guess who asked. A consent naming any other predication
//! is an unrendered slot.
//!
//! The table is a temporary owner. Each row speaks for an inventory entry and
//! names the gloss it speaks for, so a row cannot outlive a change of what its id
//! means. `language-ontology` moves the rows into ontology documents.
use std::fmt;
use serde::Serialize;
use crate::entity::Entity;
use crate::inventory::{Family, InvId, Inventory};
use crate::packet::{EntityId, NodeId, Predication};
use crate::phones::{Phone, PhoneInventory, SegmentError};
use crate::store::EntityStore;
use crate::validate::LoweringInput;
/// The language tag this lowerer's entity forms are read under, spelled as an
/// entity's `LanguageForms::language` and a corpus item's language are.
pub const TARGET_LANGUAGE: &str = "en-US";
/// The lowerer's whole view of the entity store: one entity by id. Narrow so a
/// test or a loop can hand it a map and the lowerer cannot write, list or
/// search.
pub trait EntityForms {
fn entity(&self, id: &EntityId) -> anyhow::Result<Option<Entity>>;
}
impl EntityForms for EntityStore {
fn entity(&self, id: &EntityId) -> anyhow::Result<Option<Entity>> {
self.get(id)
}
}
/// `(inventory id, the gloss the row speaks for, surface)`.
type Row = (&'static str, &'static str, &'static str);
/// Referent concepts, as the noun phrase after "the".
const CONCEPTS: &[Row] = &[
("concept:3", "player", "player"),
("concept:4", "control", "control"),
("concept:5", "ship_mind", "ship mind"),
// The Dao De Jing test bed (`daodejing.rs`). Nouns, then the verbs a
// `{verb}` frame speaks.
("concept:7", "sage", "sage"),
("concept:8", "mind", "minds"),
("concept:9", "belly", "bellies"),
("concept:10", "will", "wills"),
("concept:11", "bone", "bones"),
("concept:12", "empty", "empties"),
("concept:13", "fill", "fills"),
("concept:14", "weaken", "weakens"),
("concept:15", "strengthen", "strengthens"),
("concept:16", "heaven", "heaven"),
("concept:17", "earth", "earth"),
("concept:18", "is_long_enduring", "is long-enduring"),
("concept:19", "continues_long", "continues long"),
("concept:20", "highest_excellence", "highest excellence"),
("concept:21", "water", "water"),
("concept:22", "true_words", "words that are strictly true"),
("concept:23", "seem_paradoxical", "seem paradoxical"),
(
"concept:24",
"space_between_heaven_and_earth",
"space between heaven and earth",
),
("concept:25", "bellows", "bellows"),
("concept:26", "compare", "compare"),
("concept:27", "resemble", "resemble"),
];
/// Frames: the clause a predication of that frame says. `{gloss}` is the filler
/// of the role whose inventory gloss it is.
const FRAMES: &[Row] = &[
(
"frame:1",
"hazard_warning",
"{agent} may endanger {patient} by touching {instrument}",
),
("frame:2", "intervention", "{agent} will stop {patient}"),
(
"frame:3",
"request_intervention",
"{agent} asked {patient} to stop",
),
// The Dao De Jing test bed. Frame 4 was written from a Train chapter, 5 and
// 6 from Validation chapters, 7 and 8 from Test chapters.
("frame:4", "act", "{agent} {verb} {patient}"),
("frame:5", "intransitive", "{experiencer} {verb}"),
("frame:6", "likeness", "{experiencer} is like {goal}"),
(
"frame:7",
"seems_paradoxical",
"{experiencer} seem paradoxical",
),
(
"frame:8",
"may_be_compared",
"{experiencer} may be compared to {goal}",
),
];
/// The frame of "{agent} asked {patient} to stop": the one frame that reads
/// the discourse's addressee consent.
const REQUEST_FRAME: &str = "frame:3";
/// Speech acts: the opener of the first sentence.
const SPEECH_ACTS: &[Row] = &[
("speech_act:1", "assert", ""),
("speech_act:2", "ask", ""),
("speech_act:5", "warn", "Warning: "),
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LowerError {
/// The packet carries a slot the lowerer does not render.
Unrendered { what: &'static str },
/// No table row for an id the packet uses.
NoRow { id: InvId },
/// The row's gloss is not the gloss the inventory gives its id at the
/// packet's revision, or the id does not resolve there.
GlossMismatch { id: InvId, row: &'static str },
/// A frame template names a role the inventory has no live entry for.
UnknownRole { frame: InvId, role: String },
/// The predication fills no role its frame's template needs.
MissingRole { predication: NodeId, role: String },
/// A referent cites an entity the entity port does not hold.
UnknownEntity { entity: EntityId },
/// The entity has no form in [`TARGET_LANGUAGE`].
NoForm { entity: EntityId },
/// IPA was requested for an entity whose [`TARGET_LANGUAGE`] citation form
/// has no ruled pronunciation. Spelling is not a pronunciation.
NoPronunciation { entity: EntityId },
/// IPA was requested for a packet that cites no entity.
NoEntityReferent,
/// Phones were requested for an entity whose ruled IPA holds a symbol the
/// language's phone inventory does not list (or the language has no
/// inventory). The lowerer does not guess a phone.
UnknownPhone {
entity: EntityId,
at: usize,
text: String,
},
/// Phones were requested for an entity whose ruled IPA has a stress mark
/// with no vowel after it.
DanglingStress { entity: EntityId, at: usize },
/// A request predication the packet does not carry the addressee's consent
/// to: no consent, or a consent that names another predication. The
/// lowerer does not guess who asked.
ConsentMissing { predication: NodeId },
/// A request predication whose agent is not the discourse addressee.
AskerNotAddressee { predication: NodeId },
/// The entity port failed to read.
EntityRead { entity: EntityId, why: String },
}
impl fmt::Display for LowerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
LowerError::Unrendered { .. } => "packet carries a slot the lowerer does not render",
LowerError::NoRow { .. } => "no table row for an inventory id the packet uses",
LowerError::GlossMismatch { .. } => "table row does not match the inventory's gloss",
LowerError::UnknownRole { .. } => "frame template names a role the inventory lacks",
LowerError::MissingRole { .. } => "predication fills no role its frame needs",
LowerError::UnknownEntity { .. } => "referent cites an entity the port does not hold",
LowerError::NoForm { .. } => "entity has no form in the target language",
LowerError::NoPronunciation { .. } => "entity has no ruled pronunciation",
LowerError::NoEntityReferent => "packet cites no entity",
LowerError::UnknownPhone { .. } => {
"ruled pronunciation holds a symbol with no listed phone"
}
LowerError::DanglingStress { .. } => "stress mark with no vowel after it",
LowerError::ConsentMissing { .. } => "request lacks the addressee's consent",
LowerError::AskerNotAddressee { .. } => "request agent is not the addressee",
LowerError::EntityRead { .. } => "entity port failed to read",
})
}
}
impl std::error::Error for LowerError {}
fn row(
table: &'static [Row],
id: InvId,
inv: &Inventory,
rev: u32,
) -> Result<&'static str, LowerError> {
let (_, gloss, surface) = table
.iter()
.find(|(key, _, _)| *key == id.to_string())
.ok_or(LowerError::NoRow { id })?;
match inv.resolve(&id, rev) {
Some(e) if e.gloss == *gloss => Ok(surface),
_ => Err(LowerError::GlossMismatch { id, row: gloss }),
}
}
pub fn lower(
input: &LoweringInput,
inv: &Inventory,
entities: &dyn EntityForms,
) -> Result<String, LowerError> {
let rev = input.inventory_rev();
let unrendered = |what| Err(LowerError::Unrendered { what });
if input.predications().is_empty() {
return unrendered("no predication");
}
if !input.discourse().features.is_empty()
|| input.referents().iter().any(|r| !r.features.is_empty())
|| input.predications().iter().any(|p| !p.features.is_empty())
{
return unrendered("features");
}
request_gate(input, inv)?;
if !input.relations().is_empty() {
return unrendered("relations");
}
if !input.extensions().is_empty() {
return unrendered("extensions");
}
let opener = match input.discourse().speech_act {
Some(act) => row(SPEECH_ACTS, act, inv, rev)?,
None => "",
};
let mut sentences = Vec::new();
for p in input.predications() {
sentences.push(clause(input, inv, entities, p)?);
}
let mentioned = |r: &NodeId| {
input
.predications()
.iter()
.any(|p| p.roles.iter().any(|f| &f.filler == r))
};
if input.referents().iter().any(|r| !mentioned(&r.id)) {
return unrendered("a referent no role mentions");
}
let mut out = String::new();
for (i, (clause, name_first)) in sentences.iter().enumerate() {
if i > 0 {
out.push(' ');
}
if i == 0 {
out.push_str(opener);
}
let mut chars = clause.chars();
if let Some(first) = chars.next() {
// A name opens its clause as written; only template words are cased.
if (i == 0 && !opener.is_empty()) || *name_first {
out.push(first);
} else {
out.extend(first.to_uppercase());
}
out.push_str(chars.as_str());
}
out.push('.');
}
Ok(out)
}
/// The one place consent is read. A request clause ([`REQUEST_FRAME`]) says what
/// the addressee did, so it needs the packet's own record of it: the discourse
/// `addressee_consent`, which validation lets only authoring or game telemetry
/// set (V15), must name that predication, and its agent must be the addressee.
/// Nothing here reads wording. A consent naming any other predication is a slot
/// the lowerer does not render.
fn request_gate(input: &LoweringInput, inv: &Inventory) -> Result<(), LowerError> {
let consent = input.discourse().addressee_consent.as_ref();
let requests = input
.predications()
.iter()
.filter(|p| p.frame.to_string() == REQUEST_FRAME);
for p in requests {
if consent != Some(&p.id) {
return Err(LowerError::ConsentMissing {
predication: p.id.clone(),
});
}
let agent = inv
.entries()
.find(|e| {
e.family() == Family::Role && e.gloss == "agent" && e.live_at(input.inventory_rev())
})
.ok_or_else(|| LowerError::UnknownRole {
frame: p.frame,
role: "agent".to_string(),
})?;
let asker = p
.roles
.iter()
.find(|r| r.role == agent.id)
.map(|r| &r.filler);
if asker.is_none() || asker != input.discourse().addressee.as_ref() {
return Err(LowerError::AskerNotAddressee {
predication: p.id.clone(),
});
}
}
match consent {
Some(_)
if !input
.predications()
.iter()
.any(|p| p.frame.to_string() == REQUEST_FRAME) =>
{
Err(LowerError::Unrendered {
what: "addressee consent",
})
}
_ => Ok(()),
}
}
/// The ruled IPA of every entity a packet cites, one per entity referent in
/// the packet's referent order, as stored: bare, stress-marked, nothing
/// derived. A rejected pronunciation is never read. An entity the port does not
/// hold, or one with no ruled pronunciation in [`TARGET_LANGUAGE`], is refused:
/// the lowerer has no grapheme-to-phoneme guess to offer in its place.
pub fn pronounce(
input: &LoweringInput,
entities: &dyn EntityForms,
) -> Result<Vec<String>, LowerError> {
Ok(ruled(input, entities)?
.into_iter()
.map(|(_, ipa)| ipa)
.collect())
}
/// One entity's ruled pronunciation as the phones line a sound consumer reads:
/// the language tag, the stored IPA it came from, and the segmented phones.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PhoneLine {
pub language: &'static str,
pub source: String,
pub phones: Vec<Phone>,
}
/// The phones line of every entity a packet cites, in [`pronounce`] order, each
/// segmented by [`TARGET_LANGUAGE`]'s phone inventory. A symbol the inventory
/// does not list refuses the whole call.
pub fn phones(
input: &LoweringInput,
entities: &dyn EntityForms,
) -> Result<Vec<PhoneLine>, LowerError> {
let inventory = PhoneInventory::for_language(TARGET_LANGUAGE);
ruled(input, entities)?
.into_iter()
.map(|(entity, source)| {
let unknown = |at, text| LowerError::UnknownPhone {
entity: entity.clone(),
at,
text,
};
let inventory = inventory.ok_or_else(|| unknown(0, source.clone()))?;
let phones = inventory.segment(&source).map_err(|e| match e {
SegmentError::UnknownPhone { at, text } => unknown(at, text),
SegmentError::DanglingStress { at } => LowerError::DanglingStress {
entity: entity.clone(),
at,
},
})?;
Ok(PhoneLine {
language: TARGET_LANGUAGE,
source,
phones,
})
})
.collect()
}
/// `(entity, ruled IPA)` for each entity referent, in referent order.
fn ruled(
input: &LoweringInput,
entities: &dyn EntityForms,
) -> Result<Vec<(EntityId, String)>, LowerError> {
let mut out = Vec::new();
for id in input.referents().iter().filter_map(|r| r.entity.as_ref()) {
let entity = entity(entities, id)?;
let p = entity
.pronunciation_in(TARGET_LANGUAGE)
.ok_or_else(|| LowerError::NoPronunciation { entity: id.clone() })?;
out.push((id.clone(), p.ipa.clone()));
}
if out.is_empty() {
return Err(LowerError::NoEntityReferent);
}
Ok(out)
}
fn clause(
input: &LoweringInput,
inv: &Inventory,
entities: &dyn EntityForms,
p: &Predication,
) -> Result<(String, bool), LowerError> {
let rev = input.inventory_rev();
let template = row(FRAMES, p.frame, inv, rev)?;
let mut said: Vec<&NodeId> = Vec::new();
let mut out = String::new();
let mut name_first = false;
let mut rest = template;
while let Some(open) = rest.find('{') {
out.push_str(&rest[..open]);
let close = rest[open..].find('}').map_or(rest.len(), |c| open + c);
let name = &rest[open + 1..close];
rest = &rest[(close + 1).min(rest.len())..];
if name == "verb" {
out.push_str(row(CONCEPTS, p.concept, inv, rev)?);
continue;
}
let role = inv
.entries()
.find(|e| e.family() == Family::Role && e.gloss == name && e.live_at(rev))
.ok_or_else(|| LowerError::UnknownRole {
frame: p.frame,
role: name.to_string(),
})?;
let fill =
p.roles
.iter()
.find(|r| r.role == role.id)
.ok_or_else(|| LowerError::MissingRole {
predication: p.id.clone(),
role: name.to_string(),
})?;
let (phrase, is_name) = filler(input, inv, entities, &fill.filler, &said)?;
name_first |= is_name && out.is_empty();
out.push_str(&phrase);
said.push(&fill.filler);
}
out.push_str(rest);
if p.roles.len() != said.len() {
return Err(LowerError::Unrendered {
what: "a role the frame does not name",
});
}
Ok((out, name_first))
}
/// A role filler as a phrase: the addressee is "you", a predication is "that",
/// another referent is its entity's citation form when it cites one, else "the"
/// and its concept word. The addressee said twice in
/// one predication is reflexive; no other referent is, because the packet has no
/// animacy to choose "themself" or "itself" by.
fn filler(
input: &LoweringInput,
inv: &Inventory,
entities: &dyn EntityForms,
node: &NodeId,
said: &[&NodeId],
) -> Result<(String, bool), LowerError> {
let again = said.contains(&node);
if let Some(r) = input.referents().iter().find(|r| &r.id == node) {
if input.discourse().addressee.as_ref() == Some(node) {
return Ok((if again { "yourself" } else { "you" }.to_string(), false));
}
if let Some(id) = &r.entity {
return Ok((name(entities, id)?, true));
}
let word = row(CONCEPTS, r.concept, inv, input.inventory_rev())?;
return Ok((format!("the {word}"), false));
}
Ok(("that".to_string(), false))
}
/// The entity's citation form in [`TARGET_LANGUAGE`].
fn name(entities: &dyn EntityForms, id: &EntityId) -> Result<String, LowerError> {
entity(entities, id)?
.forms_in(TARGET_LANGUAGE)
.and_then(<[String]>::first)
.cloned()
.ok_or_else(|| LowerError::NoForm { entity: id.clone() })
}
fn entity(entities: &dyn EntityForms, id: &EntityId) -> Result<Entity, LowerError> {
entities
.entity(id)
.map_err(|e| LowerError::EntityRead {
entity: id.clone(),
why: e.to_string(),
})?
.ok_or_else(|| LowerError::UnknownEntity { entity: id.clone() })
}