1use crate::{Irtg, Symbol};
12use packed_term_arena::parser::parse_tree;
13use packed_term_arena::tree::{Tree, TreeArena};
14use std::any::Any;
15use std::io::{self, BufRead, BufReader, Read, Write};
16use thiserror::Error;
17
18const NULL_MARKER: &str = "_null_";
20
21pub struct InterpObject {
24 pub name: String,
26 pub text: String,
28 pub value: Option<Box<dyn Any + Send>>,
30}
31
32pub struct Instance {
34 pub objects: Vec<InterpObject>,
36 pub gold_derivation: Option<(TreeArena<String>, Tree)>,
38}
39
40impl Instance {
41 pub fn text(&self, name: &str) -> Option<&str> {
43 self.objects
44 .iter()
45 .find(|o| o.name == name)
46 .map(|o| o.text.as_str())
47 }
48}
49
50pub struct Corpus {
52 pub annotated: bool,
54 pub comment_prefix: String,
56 pub interpretation_order: Vec<String>,
58 pub instances: Vec<Instance>,
60}
61
62#[derive(Debug, Error)]
64pub enum CorpusError {
65 #[error("io error: {0}")]
67 Io(#[from] io::Error),
68 #[error("invalid corpus header (expected '<prefix>IRTG [un]annotated corpus file, v1.0')")]
70 BadHeader,
71 #[error("unsupported corpus version (expected v1.0)")]
73 BadVersion,
74 #[error("line {line}: malformed instance (expected {expected} lines, found {found})")]
76 MalformedInstance {
77 line: usize,
79 expected: usize,
81 found: usize,
83 },
84 #[error("line {line}: cannot parse object for interpretation {interpretation:?}: {message}")]
86 ObjectParse {
87 line: usize,
89 interpretation: String,
91 message: String,
93 },
94 #[error("line {line}: cannot parse derivation tree: {message}")]
96 TreeParse {
97 line: usize,
99 message: String,
101 },
102}
103
104pub fn read_corpus<R: Read>(
110 reader: R,
111 irtg: &Irtg,
112 limit: Option<usize>,
113) -> Result<Corpus, CorpusError> {
114 let all: Vec<String> = BufReader::new(reader).lines().collect::<io::Result<_>>()?;
115
116 let mut i = 0usize;
117
118 while i < all.len() && all[i].trim().is_empty() {
120 i += 1;
121 }
122 let header = all.get(i).ok_or(CorpusError::BadHeader)?;
123 let pos = header.find("IRTG").ok_or(CorpusError::BadHeader)?;
124 let marker = header[..pos].trim_end().to_string();
125 let rest = &header[pos..];
126 if !rest.contains("1.0") {
127 return Err(CorpusError::BadVersion);
128 }
129 let annotated = if rest.contains("unannotated") {
130 false
131 } else if rest.contains("annotated") {
132 true
133 } else {
134 return Err(CorpusError::BadHeader);
135 };
136 i += 1;
137
138 let mut interpretation_order = Vec::new();
139 while i < all.len() {
140 let line = &all[i];
141 if line.trim().is_empty() {
142 i += 1;
143 break; }
145 if !marker.is_empty() && line.starts_with(&marker) {
146 let content = line[marker.len()..].trim_start();
147 if let Some(decl) = content.strip_prefix("interpretation ") {
148 let name = decl.split(':').next().unwrap_or("").trim().to_string();
149 assert!(
150 irtg.interpretation_ref(&name).is_some(),
151 "corpus declares interpretation {name:?} which the IRTG does not contain",
152 );
153 interpretation_order.push(name);
154 }
155 i += 1;
156 } else {
157 break; }
159 }
160
161 let n = interpretation_order.len();
163 let block_size = n + usize::from(annotated);
164 let mut instances = Vec::new();
165
166 while i < all.len() {
167 if limit.is_some_and(|l| instances.len() >= l) {
168 break;
169 }
170
171 let mut block: Vec<usize> = Vec::with_capacity(block_size);
174 while block.len() < block_size {
175 while i < all.len() && all[i].trim().is_empty() {
176 i += 1;
177 }
178 if i >= all.len() {
179 break;
180 }
181 block.push(i);
182 i += 1;
183 }
184 if block.is_empty() {
185 break; }
187 if block.len() != block_size {
188 return Err(CorpusError::MalformedInstance {
189 line: block[0] + 1,
190 expected: block_size,
191 found: block.len(),
192 });
193 }
194
195 let mut objects = Vec::with_capacity(n);
196 for (k, name) in interpretation_order.iter().enumerate() {
197 let lineno = block[k];
198 let text = &all[lineno];
199 let interp = irtg
200 .interpretation_ref(name)
201 .expect("interpretation validated while reading the header");
202 let value =
205 if interp.is_inputable() {
206 Some(interp.parse_object_erased(text).map_err(|err| {
207 CorpusError::ObjectParse {
208 line: lineno + 1,
209 interpretation: name.clone(),
210 message: err.to_string(),
211 }
212 })?)
213 } else {
214 None
215 };
216 objects.push(InterpObject {
217 name: name.clone(),
218 text: text.clone(),
219 value,
220 });
221 }
222
223 let gold_derivation = if annotated {
224 let lineno = block[n];
225 let mut arena = TreeArena::new();
226 let root =
227 parse_tree(&mut arena, &all[lineno]).map_err(|err| CorpusError::TreeParse {
228 line: lineno + 1,
229 message: err.to_string(),
230 })?;
231 Some((arena, root))
232 } else {
233 None
234 };
235
236 instances.push(Instance {
237 objects,
238 gold_derivation,
239 });
240 }
241
242 Ok(Corpus {
243 annotated,
244 comment_prefix: marker,
245 interpretation_order,
246 instances,
247 })
248}
249
250pub struct CorpusWriter<W: Write> {
253 writer: W,
254 annotated: bool,
255}
256
257impl<W: Write> CorpusWriter<W> {
258 pub fn new(
261 mut writer: W,
262 comment_lines: &[String],
263 prefix: &str,
264 interpretations: &[(String, String)],
265 annotated: bool,
266 ) -> io::Result<Self> {
267 let kind = if annotated {
268 "annotated"
269 } else {
270 "unannotated"
271 };
272 let blank = prefix.trim_end();
273 writeln!(writer, "{prefix}IRTG {kind} corpus file, v1.0")?;
274 writeln!(writer, "{blank}")?;
275 for line in comment_lines {
276 writeln!(writer, "{prefix}{line}")?;
277 }
278 writeln!(writer, "{blank}")?;
279 for (name, class) in interpretations {
280 writeln!(writer, "{prefix}interpretation {name}: {class}")?;
281 }
282 writeln!(writer)?;
283 writer.flush()?;
284 Ok(Self { writer, annotated })
285 }
286
287 pub fn write_instance(
294 &mut self,
295 irtg: &Irtg,
296 interp_order: &[String],
297 derivation: Option<(&TreeArena<Symbol>, Tree)>,
298 fallback: &Instance,
299 ) -> io::Result<()> {
300 for name in interp_order {
301 let line = match derivation {
302 Some((arena, root)) => irtg
303 .interpretation_ref(name)
304 .expect("interpretation present")
305 .interpret_to_string(arena, root)
306 .map_err(|err| io::Error::other(err.to_string()))?,
307 None => fallback.text(name).unwrap_or(NULL_MARKER).to_string(),
308 };
309 writeln!(self.writer, "{line}")?;
310 }
311
312 if self.annotated {
313 let tree_line = match derivation {
314 Some((arena, root)) => {
315 let (resolved, resolved_root) =
316 irtg.grammar_signature().resolve_tree(arena, root);
317 resolved_root.display(&resolved).to_string()
318 }
319 None => NULL_MARKER.to_string(),
320 };
321 writeln!(self.writer, "{tree_line}")?;
322 }
323
324 writeln!(self.writer)?;
325 self.writer.flush()
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::{MaterializationStrategy, parse_irtg};
333
334 const GRAMMAR: &str = "\
335interpretation i: de.up.ling.irtg.algebra.StringAlgebra
336
337S! -> r1(NP,VP)
338 [i] *(?1,?2)
339NP -> r2
340 [i] john
341NP -> r3
342 [i] mary
343VP -> r4(V,NP)
344 [i] *(?1,?2)
345V -> r5
346 [i] watches
347";
348
349 fn parse_best(irtg: &Irtg, instance: &mut Instance) -> Option<crate::ViterbiTree> {
350 let mut inputs = Vec::new();
351 for obj in &mut instance.objects {
352 let value = obj.value.take().unwrap();
353 inputs.push(
354 irtg.interpretation_ref(&obj.name)
355 .unwrap()
356 .input_erased(value),
357 );
358 }
359 irtg.best_with(inputs, &MaterializationStrategy::TopDownCondensed)
360 .unwrap()
361 }
362
363 #[test]
364 fn reads_unannotated_corpus() {
365 let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
366 let text = "# IRTG unannotated corpus file, v1.0\n\
367 # interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
368 john watches mary\nmary watches john\n";
369 let corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
370
371 assert!(!corpus.annotated);
372 assert_eq!(corpus.comment_prefix, "#");
373 assert_eq!(corpus.interpretation_order, vec!["i".to_string()]);
374 assert_eq!(corpus.instances.len(), 2);
375 assert_eq!(corpus.instances[0].text("i"), Some("john watches mary"));
376 }
377
378 #[test]
379 fn limit_caps_instances() {
380 let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
381 let text = "# IRTG unannotated corpus file, v1.0\n\
382 # interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
383 john watches mary\nmary watches john\n";
384 let corpus = read_corpus(text.as_bytes(), &irtg, Some(1)).unwrap();
385 assert_eq!(corpus.instances.len(), 1);
386 }
387
388 #[test]
389 fn write_instance_emits_yield_and_tree() {
390 let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
391 let text = "# IRTG unannotated corpus file, v1.0\n\
392 # interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
393 john watches mary\n";
394 let mut corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
395
396 let best = parse_best(&irtg, &mut corpus.instances[0]);
397 let tree = best.expect("parse should succeed");
398
399 let interp = irtg.interpretation_ref("i").unwrap();
401 assert_eq!(
402 interp
403 .interpret_to_string(tree.arena(), tree.root())
404 .unwrap(),
405 "john watches mary",
406 );
407
408 let mut buf = Vec::new();
409 let interps = vec![(
410 "i".to_string(),
411 "de.up.ling.irtg.algebra.StringAlgebra".to_string(),
412 )];
413 {
414 let mut writer = CorpusWriter::new(&mut buf, &[], "# ", &interps, true).unwrap();
415 writer
416 .write_instance(
417 &irtg,
418 &corpus.interpretation_order,
419 Some((tree.arena(), tree.root())),
420 &corpus.instances[0],
421 )
422 .unwrap();
423 }
424
425 let out = String::from_utf8(buf).unwrap();
426 assert!(out.contains("# IRTG annotated corpus file, v1.0"));
427 assert!(out.contains("# interpretation i: de.up.ling.irtg.algebra.StringAlgebra"));
428 assert!(out.contains("\njohn watches mary\n"));
429 assert!(out.contains("r1(r2, r4(r5, r3))")); }
431
432 #[test]
433 fn write_instance_uses_null_marker_for_failed_parse() {
434 let irtg = parse_irtg(GRAMMAR.as_bytes()).unwrap();
435 let text = "# IRTG unannotated corpus file, v1.0\n\
436 # interpretation i: de.up.ling.irtg.algebra.StringAlgebra\n\n\
437 john watches mary\n";
438 let corpus = read_corpus(text.as_bytes(), &irtg, None).unwrap();
439
440 let mut buf = Vec::new();
441 let interps = vec![(
442 "i".to_string(),
443 "de.up.ling.irtg.algebra.StringAlgebra".to_string(),
444 )];
445 {
446 let mut writer = CorpusWriter::new(&mut buf, &[], "# ", &interps, true).unwrap();
447 writer
448 .write_instance(
449 &irtg,
450 &corpus.interpretation_order,
451 None,
452 &corpus.instances[0],
453 )
454 .unwrap();
455 }
456
457 let out = String::from_utf8(buf).unwrap();
458 assert!(out.contains("\njohn watches mary\n_null_\n")); }
460}