Lowerer reads entity forms: entity referents lower to the entity's en-US citation form #11
7 changed files with 252 additions and 36 deletions
|
|
@ -22,7 +22,7 @@ pub use inventory::{
|
|||
Change, Family, InvId, Inventory, InventoryEntry, InventoryRevision, OptionLabel, OptionSet,
|
||||
Slot, ValueKind,
|
||||
};
|
||||
pub use lower::{LowerError, lower};
|
||||
pub use lower::{EntityForms, LowerError, TARGET_LANGUAGE, lower};
|
||||
pub use packet::{
|
||||
EncoderId, EntityId, Grade, NodeId, Packet, PacketId, PacketKind, SCHEMA_VERSION, Stance,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,16 +8,44 @@
|
|||
//! 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.
|
||||
//!
|
||||
//! 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 crate::entity::Entity;
|
||||
use crate::inventory::{Family, InvId, Inventory};
|
||||
use crate::packet::{NodeId, Predication};
|
||||
use crate::packet::{EntityId, NodeId, Predication};
|
||||
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);
|
||||
|
||||
|
|
@ -36,11 +64,7 @@ const FRAMES: &[Row] = &[
|
|||
"hazard_warning",
|
||||
"{agent} may endanger {patient} by touching {instrument}",
|
||||
),
|
||||
(
|
||||
"frame:2",
|
||||
"intervention",
|
||||
"{agent} will prevent {patient} to protect {beneficiary}",
|
||||
),
|
||||
("frame:2", "intervention", "{agent} will stop {patient}"),
|
||||
];
|
||||
|
||||
/// Speech acts: the opener of the first sentence.
|
||||
|
|
@ -62,6 +86,12 @@ pub enum LowerError {
|
|||
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 },
|
||||
/// The entity port failed to read.
|
||||
EntityRead { entity: EntityId, why: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for LowerError {
|
||||
|
|
@ -88,7 +118,11 @@ fn row(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn lower(input: &LoweringInput, inv: &Inventory) -> Result<String, LowerError> {
|
||||
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() {
|
||||
|
|
@ -112,7 +146,7 @@ pub fn lower(input: &LoweringInput, inv: &Inventory) -> Result<String, LowerErro
|
|||
};
|
||||
let mut sentences = Vec::new();
|
||||
for p in input.predications() {
|
||||
sentences.push(clause(input, inv, p)?);
|
||||
sentences.push(clause(input, inv, entities, p)?);
|
||||
}
|
||||
let mentioned = |r: &NodeId| {
|
||||
input
|
||||
|
|
@ -124,7 +158,7 @@ pub fn lower(input: &LoweringInput, inv: &Inventory) -> Result<String, LowerErro
|
|||
return unrendered("a referent no role mentions");
|
||||
}
|
||||
let mut out = String::new();
|
||||
for (i, clause) in sentences.iter().enumerate() {
|
||||
for (i, (clause, name_first)) in sentences.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(' ');
|
||||
}
|
||||
|
|
@ -133,7 +167,8 @@ pub fn lower(input: &LoweringInput, inv: &Inventory) -> Result<String, LowerErro
|
|||
}
|
||||
let mut chars = clause.chars();
|
||||
if let Some(first) = chars.next() {
|
||||
if i == 0 && !opener.is_empty() {
|
||||
// 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());
|
||||
|
|
@ -145,11 +180,17 @@ pub fn lower(input: &LoweringInput, inv: &Inventory) -> Result<String, LowerErro
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
fn clause(input: &LoweringInput, inv: &Inventory, p: &Predication) -> Result<String, LowerError> {
|
||||
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]);
|
||||
|
|
@ -171,7 +212,9 @@ fn clause(input: &LoweringInput, inv: &Inventory, p: &Predication) -> Result<Str
|
|||
predication: p.id.clone(),
|
||||
role: name.to_string(),
|
||||
})?;
|
||||
out.push_str(&filler(input, inv, &fill.filler, &said)?);
|
||||
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);
|
||||
|
|
@ -180,26 +223,47 @@ fn clause(input: &LoweringInput, inv: &Inventory, p: &Predication) -> Result<Str
|
|||
what: "a role the frame does not name",
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
Ok((out, name_first))
|
||||
}
|
||||
|
||||
/// A role filler as a phrase: the addressee is "you", a predication is "that",
|
||||
/// another referent is "the" and its concept word. The addressee said twice in
|
||||
/// 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, LowerError> {
|
||||
) -> 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());
|
||||
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}"));
|
||||
return Ok((format!("the {word}"), false));
|
||||
}
|
||||
Ok("that".to_string())
|
||||
Ok(("that".to_string(), false))
|
||||
}
|
||||
|
||||
/// The entity's citation form in [`TARGET_LANGUAGE`].
|
||||
fn name(entities: &dyn EntityForms, id: &EntityId) -> Result<String, LowerError> {
|
||||
let entity = entities
|
||||
.entity(id)
|
||||
.map_err(|e| LowerError::EntityRead {
|
||||
entity: id.clone(),
|
||||
why: e.to_string(),
|
||||
})?
|
||||
.ok_or_else(|| LowerError::UnknownEntity { entity: id.clone() })?;
|
||||
entity
|
||||
.forms_in(TARGET_LANGUAGE)
|
||||
.and_then(<[String]>::first)
|
||||
.cloned()
|
||||
.ok_or_else(|| LowerError::NoForm { entity: id.clone() })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -502,7 +502,10 @@ fn main() -> Result<()> {
|
|||
&EntityStore::open(&cli.entities)?,
|
||||
)
|
||||
.map_err(refused)?;
|
||||
println!("{}", lower(&view, &inventory)?);
|
||||
println!(
|
||||
"{}",
|
||||
lower(&view, &inventory, &EntityStore::open(&cli.entities)?)?
|
||||
);
|
||||
}
|
||||
Command::Score(args) => {
|
||||
let output = match &args.output {
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ pub struct Trace {
|
|||
}
|
||||
|
||||
/// The `nibu-proof-001` packet: the meaning of `examples/english-lowering/nibu-proof-001/interlingua.yaml`
|
||||
/// (warn the player off the damaged control; Nibu prevents it for them) as typed
|
||||
/// (warn the player off the damaged control; Nibu will stop it) as typed
|
||||
/// ids of inventory revision 2, for the corpus item `item:nibu:1`. No features:
|
||||
/// nothing here is style.
|
||||
pub fn proof_packet() -> Packet {
|
||||
|
|
@ -408,11 +408,7 @@ pub fn proof_packet() -> Packet {
|
|||
id: node("n5"),
|
||||
concept: id("concept:2"),
|
||||
frame: id("frame:2"),
|
||||
roles: vec![
|
||||
fill("role:1", "n3"),
|
||||
fill("role:2", "n4"),
|
||||
fill("role:8", "n1"),
|
||||
],
|
||||
roles: vec![fill("role:1", "n3"), fill("role:2", "n4")],
|
||||
features: vec![],
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -807,7 +807,7 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() {
|
|||
"--slug",
|
||||
"nibu",
|
||||
"--text",
|
||||
"Warning: you may endanger yourself by touching the control. The ship mind will prevent that to protect you.",
|
||||
"Warning: you may endanger yourself by touching the control. NIBU will stop that.",
|
||||
"--speaker",
|
||||
"entity:nibu",
|
||||
"--group",
|
||||
|
|
@ -832,6 +832,8 @@ fn proof_packet_lowers_and_the_lowering_scores_against_items() {
|
|||
text.starts_with("Warning: you may endanger yourself"),
|
||||
"{text}"
|
||||
);
|
||||
// The referent cites entity:nibu: its stored en-US form (NIBU here) reaches the text as stored.
|
||||
assert!(text.contains("NIBU will stop"), "{text}");
|
||||
|
||||
let missing = weksa(d.path(), &["lower", "./p.cc", "packet:0000000000000000"]);
|
||||
assert!(!missing.status.success() && out(&missing).contains("no packet"));
|
||||
|
|
|
|||
|
|
@ -6,15 +6,16 @@ mod common;
|
|||
use std::path::Path;
|
||||
|
||||
use common::{iv, nd};
|
||||
use weksa::entity::{Entity, EntityOrigin, LanguageForms};
|
||||
use weksa::inventory::proof_revision;
|
||||
use weksa::packet::{FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::packet::{EntityId, FeatValue, FeatureValue, Packet, proof_packet};
|
||||
use weksa::{
|
||||
CorpusStore, EntityStore, Inventory, InventoryStore, LowerError, LoweringInput, PacketStore,
|
||||
lower, validate,
|
||||
CorpusStore, EntityForms, EntityStore, Inventory, InventoryStore, LowerError, LoweringInput,
|
||||
PacketStore, lower, validate,
|
||||
};
|
||||
|
||||
const PROOF: &str = "Warning: you may endanger yourself by touching the control. \
|
||||
The ship mind will prevent that to protect you.";
|
||||
Nibu will stop that.";
|
||||
|
||||
fn committed() -> (Inventory, tempfile::TempDir) {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
|
|
@ -42,8 +43,13 @@ fn admitted(packet: &Packet, inventory: &InventoryStore) -> LoweringInput {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
fn committed_entities() -> EntityStore {
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
EntityStore::open(&root.join("data/entity/entity.cc")).unwrap()
|
||||
}
|
||||
|
||||
fn lowered(packet: &Packet, inv: &Inventory) -> Result<String, LowerError> {
|
||||
lower(&view(packet, inv), inv)
|
||||
lower(&view(packet, inv), inv, &committed_entities())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -69,7 +75,7 @@ fn only_the_addressee_is_you_and_is_reflexive() {
|
|||
assert_eq!(
|
||||
lowered(&p, &inv).unwrap(),
|
||||
"Warning: the player may endanger the player by touching the control. \
|
||||
The ship mind will prevent that to protect the player."
|
||||
Nibu will stop that."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +159,7 @@ fn a_packet_carrying_what_the_lowerer_does_not_render_is_refused() {
|
|||
let mut p = proof_packet();
|
||||
edit(&mut p);
|
||||
assert_eq!(
|
||||
lower(&admitted(&p, &store), &inv),
|
||||
lower(&admitted(&p, &store), &inv, &committed_entities()),
|
||||
Err(LowerError::Unrendered { what }),
|
||||
"{what}"
|
||||
);
|
||||
|
|
@ -203,10 +209,150 @@ fn a_row_cannot_speak_for_an_id_whose_gloss_differs() {
|
|||
store.admit_revision(rev2, entries).unwrap();
|
||||
let inv = store.inventory().unwrap();
|
||||
assert_eq!(
|
||||
lower(&admitted(&proof_packet(), &store), &inv),
|
||||
lower(
|
||||
&admitted(&proof_packet(), &store),
|
||||
&inv,
|
||||
&committed_entities()
|
||||
),
|
||||
Err(LowerError::GlossMismatch {
|
||||
id: iv("concept:4"),
|
||||
row: "control"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// An entity port a test controls: ids to entities, and an optional read fault.
|
||||
#[derive(Default)]
|
||||
struct Entities {
|
||||
held: Vec<Entity>,
|
||||
fault: bool,
|
||||
}
|
||||
|
||||
impl EntityForms for Entities {
|
||||
fn entity(&self, id: &EntityId) -> anyhow::Result<Option<Entity>> {
|
||||
if self.fault {
|
||||
anyhow::bail!("store unreadable");
|
||||
}
|
||||
Ok(self.held.iter().find(|e| &e.id == id).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
fn nibu(forms: &[(&str, &[&str])]) -> Entity {
|
||||
Entity {
|
||||
id: EntityId::parse("entity:nibu").unwrap(),
|
||||
forms: forms
|
||||
.iter()
|
||||
.map(|(language, forms)| LanguageForms {
|
||||
language: (*language).into(),
|
||||
forms: forms.iter().map(|f| (*f).into()).collect(),
|
||||
})
|
||||
.collect(),
|
||||
origin: EntityOrigin::Cast {
|
||||
persona: "nibu".into(),
|
||||
fixture: "f.yaml".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn through(entities: &Entities) -> Result<String, LowerError> {
|
||||
let (inv, _dir) = committed();
|
||||
lower(&view(&proof_packet(), &inv), &inv, entities)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_name_survives_lowering_verbatim_where_the_concept_word_was() {
|
||||
// The entity's own form, not "the ship mind", and not re-cased or inflected:
|
||||
// the form is lowercase and stays so mid-sentence.
|
||||
let mut e = Entities::default();
|
||||
let n = nibu(&[("en-US", &["nibu-of-the-deep"])]);
|
||||
e.held.push(n);
|
||||
// It opens the second sentence, where a template word would be capitalised.
|
||||
assert_eq!(
|
||||
through(&e).unwrap(),
|
||||
"Warning: you may endanger yourself by touching the control. \
|
||||
nibu-of-the-deep will stop that."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_target_languages_citation_form_is_chosen() {
|
||||
// Forms after the first are inflected shapes no English frame asks for; a
|
||||
// form under another language tag is never read, whatever order it is in.
|
||||
let mut e = Entities::default();
|
||||
let n = nibu(&[
|
||||
("sa-Latn", &["Nibuwa"]),
|
||||
("en-US", &["Nibu", "Nibu's", "Nibus"]),
|
||||
]);
|
||||
e.held.push(n);
|
||||
assert_eq!(through(&e).unwrap(), PROOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entity_the_port_does_not_hold_is_refused_not_replaced_by_its_concept_word() {
|
||||
let id = EntityId::parse("entity:nibu").unwrap();
|
||||
assert_eq!(
|
||||
through(&Entities::default()),
|
||||
Err(LowerError::UnknownEntity { entity: id })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entity_with_no_target_language_form_is_refused() {
|
||||
let mut e = Entities::default();
|
||||
let n = nibu(&[("sa-Latn", &["Nibuwa"])]);
|
||||
let id = n.id.clone();
|
||||
e.held.push(n);
|
||||
assert_eq!(through(&e), Err(LowerError::NoForm { entity: id }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failing_entity_port_is_refused_by_name() {
|
||||
let e = Entities {
|
||||
fault: true,
|
||||
..Entities::default()
|
||||
};
|
||||
assert_eq!(
|
||||
through(&e),
|
||||
Err(LowerError::EntityRead {
|
||||
entity: EntityId::parse("entity:nibu").unwrap(),
|
||||
why: "store unreadable".into()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_addressee_entity_still_lowers_to_you_and_is_never_looked_up() {
|
||||
// Naming the addressee would break "you"/"yourself": the port is not asked,
|
||||
// so a port that faults on every read changes nothing.
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = proof_packet();
|
||||
p.referents[0].entity = Some(EntityId::parse("entity:nibu").unwrap());
|
||||
p.referents[2].entity = None;
|
||||
let faulting = Entities {
|
||||
fault: true,
|
||||
..Entities::default()
|
||||
};
|
||||
assert_eq!(
|
||||
lower(&view(&p, &inv), &inv, &faulting).unwrap(),
|
||||
"Warning: you may endanger yourself by touching the control. \
|
||||
The ship mind will stop that."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_purpose_the_frame_does_not_say_is_refused_not_dropped() {
|
||||
// The intervention row says "will stop"; a packet that also fills the
|
||||
// beneficiary role would assert a purpose the text does not carry.
|
||||
let (inv, _dir) = committed();
|
||||
let mut p = proof_packet();
|
||||
p.predications[1].roles.push(weksa::packet::RoleFill {
|
||||
role: iv("role:8"),
|
||||
filler: nd("n1"),
|
||||
});
|
||||
assert_eq!(
|
||||
lowered(&p, &inv),
|
||||
Err(LowerError::Unrendered {
|
||||
what: "a role the frame does not name"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,7 +78,12 @@ fn a_scene_relation_reaches_the_lowering_view_and_changes_no_english() {
|
|||
];
|
||||
let with = plain.clone().with_scene(&inv, scene.clone()).unwrap();
|
||||
assert_eq!(with.scene(), scene.as_slice());
|
||||
assert_eq!(lower(&with, &inv).unwrap(), lower(&plain, &inv).unwrap());
|
||||
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||
let entities = EntityStore::open(&root.join("data/entity/entity.cc")).unwrap();
|
||||
assert_eq!(
|
||||
lower(&with, &inv, &entities).unwrap(),
|
||||
lower(&plain, &inv, &entities).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue