use crate::{Irtg, Symbol};
use packed_term_arena::parser::parse_tree;
use packed_term_arena::tree::{Tree, TreeArena};
use std::any::Any;
use std::io::{self, BufRead, BufReader, Read, Write};
use thiserror::Error;
const NULL_MARKER: &str = "_null_";
pub struct InterpObject {
pub name: String,
pub text: String,
pub value: Option<Box<dyn Any + Send>>,
}
pub struct Instance {
pub objects: Vec<InterpObject>,
pub gold_derivation: Option<(TreeArena<String>, Tree)>,
}
impl Instance {
pub fn text(&self, name: &str) -> Option<&str> {
self.objects
.iter()
.find(|o| o.name == name)
.map(|o| o.text.as_str())
}
}
pub struct Corpus {
pub annotated: bool,
pub comment_prefix: String,
pub interpretation_order: Vec<String>,
pub instances: Vec<Instance>,
}
#[derive(Debug, Error)]
pub enum CorpusError {
#[error("io error: {0}")]
Io(#[from] io::Error),
#[error("invalid corpus header (expected '<prefix>IRTG [un]annotated corpus file, v1.0')")]
BadHeader,
#[error("unsupported corpus version (expected v1.0)")]
BadVersion,
#[error("line {line}: malformed instance (expected {expected} lines, found {found})")]
MalformedInstance {
line: usize,
expected: usize,
found: usize,
},
#[error("line {line}: cannot parse object for interpretation {interpretation:?}: {message}")]
ObjectParse {
line: usize,
interpretation: String,
message: String,
},
#[error("line {line}: cannot parse derivation tree: {message}")]
TreeParse {
line: usize,
message: String,
},
}
pub fn read_corpus<R: Read>(
reader: R,
irtg: &Irtg,
limit: Option<usize>,
) -> Result<Corpus, CorpusError> {
let all: Vec<String> = BufReader::new(reader).lines().collect::<io::Result<_>>()?;
let mut i = 0usize;
while i < all.len() && all[i].trim().is_empty() {
i += 1;
}
let header = all.get(i).ok_or(CorpusError::BadHeader)?;
let pos = header.find("IRTG").ok_or(CorpusError::BadHeader)?;
let marker = header[..pos].trim_end().to_string();
let rest = &header[pos..];
if !rest.contains("1.0") {
return Err(CorpusError::BadVersion);
}
let annotated = if rest.contains("unannotated") {
false
} else if rest.contains("annotated") {
true
} else {
return Err(CorpusError::BadHeader);
};
i += 1;
let mut interpretation_order = Vec::new();
while i < all.len() {
let line = &all[i];
if line.trim().is_empty() {
i += 1;
break; }
if !marker.is_empty() && line.starts_with(&marker) {
let content = line[marker.len()..].trim_start();
if let Some(decl) = content.strip_prefix("interpretation ") {
let name = decl.split(':').next().unwrap_or("").trim().to_string();
assert!(
irtg.interpretation_ref(&name).is_some(),
"corpus declares interpretation {name:?} which the IRTG does not contain",
);
interpretation_order.push(name);
}
i += 1;
} else {
break; }
}
let n = interpretation_order.len();
let block_size = n + usize::from(annotated);
let mut instances = Vec::new();
while i < all.len() {
if limit.is_some_and(|l| instances.len() >= l) {
break;
}
let mut block: Vec<usize> = Vec::with_capacity(block_size);
while block.len() < block_size {
while i < all.len() && all[i].trim().is_empty() {
i += 1;
}
if i >= all.len() {
break;
}
block.push(i);
i += 1;
}
if block.is_empty() {
break; }
if block.len() != block_size {
return Err(CorpusError::MalformedInstance {
line: block[0] + 1,
expected: block_size,
found: block.len(),
});
}
let mut objects = Vec::with_capacity(n);
for (k, name) in interpretation_order.iter().enumerate() {
let lineno = block[k];
let text = &all[lineno];
let interp = irtg
.interpretation_ref(name)
.expect("interpretation validated while reading the header");
let value =
if interp.is_inputable() {
Some(interp.parse_object_erased(text).map_err(|err| {
CorpusError::ObjectParse {
line: lineno + 1,
interpretation: name.clone(),
message: err.to_string(),
}
})?)
} else {
None
};
objects.push(InterpObject {
name: name.clone(),
text: text.clone(),
value,
});
}
let gold_derivation = if annotated {
let lineno = block[n];
let mut arena = TreeArena::new();
let root =
parse_tree(&mut arena, &all[lineno]).map_err(|err| CorpusError::TreeParse {
line: lineno + 1,
message: err.to_string(),
})?;
Some((arena, root))
} else {
None
};
instances.push(Instance {
objects,
gold_derivation,
});
}
Ok(Corpus {
annotated,
comment_prefix: marker,
interpretation_order,
instances,
})
}
pub struct CorpusWriter<W: Write> {
writer: W,
annotated: bool,
}
impl<W: Write> CorpusWriter<W> {
pub fn new(
mut writer: W,
comment_lines: &[String],
prefix: &str,
interpretations: &[(String, String)],
annotated: bool,
) -> io::Result<Self> {
let kind = if annotated {
"annotated"
} else {
"unannotated"
};
let blank = prefix.trim_end();
writeln!(writer, "{prefix}IRTG {kind} corpus file, v1.0")?;
writeln!(writer, "{blank}")?;
for line in comment_lines {
writeln!(writer, "{prefix}{line}")?;
}
writeln!(writer, "{blank}")?;
for (name, class) in interpretations {
writeln!(writer, "{prefix}interpretation {name}: {class}")?;
}
writeln!(writer)?;
writer.flush()?;
Ok(Self { writer, annotated })
}
pub fn write_instance(
&mut self,
irtg: &Irtg,
interp_order: &[String],
derivation: Option<(&TreeArena<Symbol>, Tree)>,
fallback: &Instance,
) -> io::Result<()> {
for name in interp_order {
let line = match derivation {
Some((arena, root)) => irtg
.interpretation_ref(name)
.expect("interpretation present")
.interpret_to_string(arena, root)
.map_err(|err| io::Error::other(err.to_string()))?,
None => fallback.text(name).unwrap_or(NULL_MARKER).to_string(),
};
writeln!(self.writer, "{line}")?;
}
if self.annotated {
let tree_line = match derivation {
Some((arena, root)) => {
let (resolved, resolved_root) =
irtg.grammar_signature().resolve_tree(arena, root);
resolved_root.display(&resolved).to_string()
}
None => NULL_MARKER.to_string(),
};
writeln!(self.writer, "{tree_line}")?;
}
writeln!(self.writer)?;
self.writer.flush()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MaterializationStrategy, parse_irtg};
const GRAMMAR: &str = "\
interpretation i: de.up.ling.irtg.algebra.StringAlgebra
S! -> r1(NP,VP)
[i] *(?1,?2)
NP -> r2
[i] john
NP -> r3
[i] mary
VP -> r4(V,NP)
[i] *(?1,?2)
V -> r5
[i] watches
";
fn parse_best(irtg: &Irtg, instance: &mut Instance) -> Option<crate::ViterbiTree> {
let mut inputs = Vec::new();
for obj in &mut instance.objects {
let value = obj.value.take().unwrap();
inputs.push(
irtg.interpretation_ref(&obj.name)
.unwrap()
.input_erased(value),
);
}
irtg.best_with(inputs, &MaterializationStrategy::TopDownCondensed)
.unwrap()
}
#[test]
fn reads_unannotated_corpus() {
let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
let text = "# IRTG unannotated corpus file, v1.0\n\
# interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
john watches mary\nmary watches john\n";
let corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
assert!(!corpus.annotated);
assert_eq!(corpus.comment_prefix, "#");
assert_eq!(corpus.interpretation_order, vec!["i".to_string()]);
assert_eq!(corpus.instances.len(), 2);
assert_eq!(corpus.instances[0].text("i"), Some("john watches mary"));
}
#[test]
fn limit_caps_instances() {
let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
let text = "# IRTG unannotated corpus file, v1.0\n\
# interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
john watches mary\nmary watches john\n";
let corpus = read_corpus(text.as_bytes(), &irtg, Some(1)).unwrap();
assert_eq!(corpus.instances.len(), 1);
}
#[test]
fn write_instance_emits_yield_and_tree() {
let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
let text = "# IRTG unannotated corpus file, v1.0\n\
# interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
john watches mary\n";
let mut corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
let best = parse_best(&irtg, &mut corpus.instances[0]);
let tree = best.expect("parse should succeed");
let interp = irtg.interpretation_ref("i").unwrap();
assert_eq!(
interp
.interpret_to_string(tree.arena(), tree.root())
.unwrap(),
"john watches mary",
);
let mut buf = Vec::new();
let interps = vec![(
"i".to_string(),
"de.up.ling.irtg.algebra.StringAlgebra".to_string(),
)];
{
let mut writer = CorpusWriter::new(&mut buf, &[], "# ", &interps, true).unwrap();
writer
.write_instance(
&irtg,
&corpus.interpretation_order,
Some((tree.arena(), tree.root())),
&corpus.instances[0],
)
.unwrap();
}
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("# IRTG annotated corpus file, v1.0"));
assert!(out.contains("# interpretation i: de.up.ling.irtg.algebra.StringAlgebra"));
assert!(out.contains("\njohn watches mary\n"));
assert!(out.contains("r1(r2, r4(r5, r3))")); }
#[test]
fn write_instance_uses_null_marker_for_failed_parse() {
let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
let text = "# IRTG unannotated corpus file, v1.0\n\
# interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
john watches mary\n";
let corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
let mut buf = Vec::new();
let interps = vec![(
"i".to_string(),
"de.up.ling.irtg.algebra.StringAlgebra".to_string(),
)];
{
let mut writer = CorpusWriter::new(&mut buf, &[], "# ", &interps, true).unwrap();
writer
.write_instance(
&irtg,
&corpus.interpretation_order,
None,
&corpus.instances[0],
)
.unwrap();
}
let out = String::from_utf8(buf).unwrap();
assert!(out.contains("\njohn watches mary\n_null_\n")); }
}