use serde::Serialize;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct LinkedArg {
pub arg: String,
pub theta_role: &'static str,
pub macrorole: &'static str,
pub relation: &'static str,
}
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct LinkReport {
pub verb: String,
pub valence: String,
pub args: Vec<LinkedArg>,
pub issues: Vec<String>,
}
fn expected_arity(valence: &str) -> Option<usize> {
match valence.to_lowercase().as_str() {
"impersonal" => Some(0),
"intransitive" => Some(1),
"transitive" => Some(2),
"ditransitive" => Some(3),
_ => None,
}
}
pub fn link(verb: &str, valence: &str, args: &[String]) -> LinkReport {
let mut r = LinkReport { verb: verb.to_string(), valence: valence.to_string(), ..Default::default() };
let expected = expected_arity(valence);
let effective_arity = expected.unwrap_or(args.len());
if expected.is_none() {
r.issues.push(format!(
"unknown valence `{valence}` — inferring {effective_arity} core argument(s) from the input"
));
} else if let Some(exp) = expected {
if exp != args.len() {
r.issues.push(format!(
"{valence} expects {exp} argument(s) but {} were given",
args.len()
));
}
}
let template: &[(&'static str, &'static str, &'static str)] = match effective_arity {
0 => &[],
1 => &[("agent", "actor", "subject")],
2 => &[("agent", "actor", "subject"), ("patient", "undergoer", "object")],
_ => &[
("agent", "actor", "subject"),
("theme", "undergoer", "object"),
("recipient", "non-macrorole", "indirect-object"),
],
};
for (i, arg) in args.iter().enumerate() {
let (theta, macro_, rel) = template.get(i).copied().unwrap_or(("oblique", "non-macrorole", "adjunct"));
r.args.push(LinkedArg { arg: arg.clone(), theta_role: theta, macrorole: macro_, relation: rel });
}
r
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_transitive_clause_gets_actor_and_undergoer() {
let r = link("see", "transitive", &["she".into(), "bird".into()]);
assert!(r.issues.is_empty());
assert_eq!(r.args[0].macrorole, "actor");
assert_eq!(r.args[0].relation, "subject");
assert_eq!(r.args[1].theta_role, "patient");
assert_eq!(r.args[1].macrorole, "undergoer");
}
#[test]
fn a_ditransitive_clause_links_a_recipient() {
let r = link("give", "ditransitive", &["she".into(), "book".into(), "him".into()]);
assert!(r.issues.is_empty());
assert_eq!(r.args[2].theta_role, "recipient");
assert_eq!(r.args[2].relation, "indirect-object");
assert_eq!(r.args[2].macrorole, "non-macrorole");
}
#[test]
fn an_arity_mismatch_is_flagged_but_still_linked() {
let r = link("sleep", "intransitive", &["she".into(), "bed".into()]);
assert_eq!(r.issues.len(), 1);
assert_eq!(r.args[0].macrorole, "actor");
assert_eq!(r.args[1].relation, "adjunct");
}
#[test]
fn an_unknown_valence_infers_from_arity() {
let r = link("frob", "quadrivalent", &["a".into(), "b".into()]);
assert!(r.issues.iter().any(|i| i.contains("inferring")));
assert_eq!(r.args.len(), 2);
assert_eq!(r.args[1].macrorole, "undergoer");
}
}