1use crate::kernel::domain::Domain;
56use crate::kernel::expr::{BigFloat, BigInt, BigRat, ExprData, ExprId, PredicateKind};
57use crate::kernel::pool::ExprPool;
58use std::fs::{self, File};
59use std::io::{self, BufReader, BufWriter, Read, Write};
60use std::path::{Path, PathBuf};
61
62const MAGIC: &[u8; 4] = b"ALKP";
63const POOL_FORMAT_V1: u32 = 1;
65const POOL_FORMAT_V2: u32 = 2;
67const POOL_FORMAT_V3: u32 = 3;
69const POOL_FORMAT_V4: u32 = 4;
71const POOL_FORMAT_V5: u32 = 5;
73const POOL_FORMAT_WRITE: u32 = POOL_FORMAT_V5;
74
75#[derive(Debug)]
83pub enum IoError {
84 Io(io::Error),
85 BadMagic,
86 UnsupportedVersion(u32),
87 Truncated,
88 BadUtf8,
89 BadDomain(u8),
90 BadTag(u8),
91 BadPredicateKind(u8),
92 BadNumeric(String),
93}
94
95impl std::fmt::Display for IoError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97 match self {
98 IoError::Io(e) => write!(f, "io error: {e}"),
99 IoError::BadMagic => write!(f, "not an alkahest pool file (bad magic)"),
100 IoError::UnsupportedVersion(v) => {
101 write!(
102 f,
103 "unsupported pool file version {v}; run `alkahest migrate-pool`"
104 )
105 }
106 IoError::Truncated => write!(f, "pool file truncated or incomplete"),
107 IoError::BadUtf8 => write!(f, "pool file contains invalid UTF-8"),
108 IoError::BadDomain(b) => write!(f, "pool file has unknown domain tag {b}"),
109 IoError::BadTag(b) => write!(f, "pool file has unknown node tag {b}"),
110 IoError::BadPredicateKind(b) => {
111 write!(f, "pool file has unknown predicate kind {b}")
112 }
113 IoError::BadNumeric(s) => write!(f, "pool file has invalid numeric: {s}"),
114 }
115 }
116}
117
118impl std::error::Error for IoError {}
119
120impl From<io::Error> for IoError {
121 fn from(e: io::Error) -> Self {
122 IoError::Io(e)
123 }
124}
125
126impl crate::errors::AlkahestError for IoError {
127 fn code(&self) -> &'static str {
128 match self {
129 IoError::Io(_) => "E-IO-001",
130 IoError::BadMagic => "E-IO-002",
131 IoError::UnsupportedVersion(_) => "E-IO-003",
132 IoError::Truncated => "E-IO-004",
133 IoError::BadUtf8 => "E-IO-005",
134 IoError::BadDomain(_) => "E-IO-006",
135 IoError::BadTag(_) => "E-IO-007",
136 IoError::BadPredicateKind(_) => "E-IO-008",
137 IoError::BadNumeric(_) => "E-IO-009",
138 }
139 }
140
141 fn remediation(&self) -> Option<&'static str> {
142 match self {
143 IoError::BadMagic => Some(
144 "file is not an alkahest pool; check the path or regenerate with ExprPool::checkpoint()",
145 ),
146 IoError::UnsupportedVersion(_) => Some(
147 "run the `alkahest migrate-pool` CLI to upgrade the file, or regenerate from source",
148 ),
149 IoError::Truncated => Some(
150 "file was truncated (likely a crash during checkpoint); rerun from source and checkpoint again",
151 ),
152 _ => None,
153 }
154 }
155}
156
157#[deprecated(since = "2.0.0", note = "renamed to IoError with E-IO-* codes")]
159pub type PoolPersistError = IoError;
160
161fn write_u8(w: &mut impl Write, v: u8) -> io::Result<()> {
166 w.write_all(&[v])
167}
168fn write_u32(w: &mut impl Write, v: u32) -> io::Result<()> {
169 w.write_all(&v.to_le_bytes())
170}
171fn write_u64(w: &mut impl Write, v: u64) -> io::Result<()> {
172 w.write_all(&v.to_le_bytes())
173}
174
175fn write_str(w: &mut impl Write, s: &str) -> io::Result<()> {
176 let bytes = s.as_bytes();
177 write_u32(w, bytes.len() as u32)?;
178 w.write_all(bytes)
179}
180
181fn write_ids(w: &mut impl Write, ids: &[ExprId]) -> io::Result<()> {
182 write_u32(w, ids.len() as u32)?;
183 for id in ids {
184 write_u32(w, id.0)?;
185 }
186 Ok(())
187}
188
189const MAX_FILE_FLOAT_PREC: u32 = 1 << 24;
196
197const MIN_NODE_BYTES: u64 = 5;
200
201struct Bounded<R> {
209 inner: R,
210 remaining: u64,
211}
212
213impl<R: Read> Bounded<R> {
214 fn new(inner: R, len: u64) -> Self {
215 Bounded {
216 inner,
217 remaining: len,
218 }
219 }
220
221 fn ensure(&self, count: u64, size: u64) -> Result<(), IoError> {
224 match count.checked_mul(size) {
225 Some(n) if n <= self.remaining => Ok(()),
226 _ => Err(IoError::Truncated),
227 }
228 }
229
230 fn read_exact(&mut self, buf: &mut [u8]) -> Result<(), IoError> {
231 self.ensure(buf.len() as u64, 1)?;
232 self.inner.read_exact(buf).map_err(|_| IoError::Truncated)?;
233 self.remaining -= buf.len() as u64;
234 Ok(())
235 }
236}
237
238fn read_u8(r: &mut Bounded<impl Read>) -> Result<u8, IoError> {
239 let mut b = [0u8; 1];
240 r.read_exact(&mut b)?;
241 Ok(b[0])
242}
243
244fn read_u32(r: &mut Bounded<impl Read>) -> Result<u32, IoError> {
245 let mut b = [0u8; 4];
246 r.read_exact(&mut b)?;
247 Ok(u32::from_le_bytes(b))
248}
249
250fn read_u64(r: &mut Bounded<impl Read>) -> Result<u64, IoError> {
251 let mut b = [0u8; 8];
252 r.read_exact(&mut b)?;
253 Ok(u64::from_le_bytes(b))
254}
255
256fn read_str(r: &mut Bounded<impl Read>) -> Result<String, IoError> {
257 let len = u64::from(read_u32(r)?);
258 r.ensure(len, 1)?;
259 let mut buf = vec![0u8; len as usize];
260 r.read_exact(&mut buf)?;
261 String::from_utf8(buf).map_err(|_| IoError::BadUtf8)
262}
263
264fn read_ids(r: &mut Bounded<impl Read>) -> Result<Vec<ExprId>, IoError> {
265 let arity = u64::from(read_u32(r)?);
266 r.ensure(arity, 4)?;
267 let mut out = Vec::with_capacity(arity as usize);
268 for _ in 0..arity {
269 out.push(ExprId(read_u32(r)?));
270 }
271 Ok(out)
272}
273
274fn domain_to_u8(d: &Domain) -> u8 {
279 match d {
280 Domain::Real => 0,
281 Domain::Complex => 1,
282 Domain::Integer => 2,
283 Domain::Positive => 3,
284 Domain::NonNegative => 4,
285 Domain::NonZero => 5,
286 }
287}
288
289fn u8_to_domain(b: u8) -> Result<Domain, IoError> {
290 match b {
291 0 => Ok(Domain::Real),
292 1 => Ok(Domain::Complex),
293 2 => Ok(Domain::Integer),
294 3 => Ok(Domain::Positive),
295 4 => Ok(Domain::NonNegative),
296 5 => Ok(Domain::NonZero),
297 b => Err(IoError::BadDomain(b)),
298 }
299}
300
301fn pred_to_u8(k: &PredicateKind) -> u8 {
302 match k {
304 PredicateKind::Eq => 0,
305 PredicateKind::Ne => 1,
306 PredicateKind::Lt => 2,
307 PredicateKind::Le => 3,
308 PredicateKind::Gt => 4,
309 PredicateKind::Ge => 5,
310 PredicateKind::And => 6,
311 PredicateKind::Or => 7,
312 PredicateKind::Not => 8,
313 PredicateKind::True => 9,
314 PredicateKind::False => 10,
315 }
316}
317
318fn u8_to_pred(b: u8) -> Result<PredicateKind, IoError> {
319 match b {
320 0 => Ok(PredicateKind::Eq),
321 1 => Ok(PredicateKind::Ne),
322 2 => Ok(PredicateKind::Lt),
323 3 => Ok(PredicateKind::Le),
324 4 => Ok(PredicateKind::Gt),
325 5 => Ok(PredicateKind::Ge),
326 6 => Ok(PredicateKind::And),
327 7 => Ok(PredicateKind::Or),
328 8 => Ok(PredicateKind::Not),
329 9 => Ok(PredicateKind::True),
330 10 => Ok(PredicateKind::False),
331 b => Err(IoError::BadPredicateKind(b)),
332 }
333}
334
335fn write_node(w: &mut impl Write, node: &ExprData) -> io::Result<()> {
340 match node {
341 ExprData::Symbol {
342 name,
343 domain,
344 commutative,
345 } => {
346 write_u8(w, 0)?;
347 write_u8(w, domain_to_u8(domain))?;
348 write_u8(w, u8::from(*commutative))?;
349 write_str(w, name)
350 }
351 ExprData::Integer(BigInt(n)) => {
352 write_u8(w, 1)?;
353 write_str(w, &n.to_string())
354 }
355 ExprData::Rational(BigRat(r)) => {
356 write_u8(w, 2)?;
357 write_str(w, &r.numer().to_string())?;
358 write_str(w, &r.denom().to_string())
359 }
360 ExprData::Float(BigFloat { inner, prec }) => {
361 write_u8(w, 3)?;
362 write_u32(w, *prec)?;
363 write_str(w, &inner.to_string_radix(16, None))
365 }
366 ExprData::Add(children) => {
367 write_u8(w, 4)?;
368 write_ids(w, children)
369 }
370 ExprData::Mul(children) => {
371 write_u8(w, 5)?;
372 write_ids(w, children)
373 }
374 ExprData::Pow { base, exp } => {
375 write_u8(w, 6)?;
376 write_u32(w, base.0)?;
377 write_u32(w, exp.0)
378 }
379 ExprData::Func { name, args } => {
380 write_u8(w, 7)?;
381 write_str(w, name)?;
382 write_ids(w, args)
383 }
384 ExprData::Piecewise { branches, default } => {
385 write_u8(w, 8)?;
386 write_u32(w, branches.len() as u32)?;
387 for (c, v) in branches {
388 write_u32(w, c.0)?;
389 write_u32(w, v.0)?;
390 }
391 write_u32(w, default.0)
392 }
393 ExprData::Predicate { kind, args } => {
394 write_u8(w, 9)?;
395 write_u8(w, pred_to_u8(kind))?;
396 write_ids(w, args)
397 }
398 ExprData::Forall { var, body } => {
399 write_u8(w, 10)?;
400 write_u32(w, var.0)?;
401 write_u32(w, body.0)
402 }
403 ExprData::Exists { var, body } => {
404 write_u8(w, 11)?;
405 write_u32(w, var.0)?;
406 write_u32(w, body.0)
407 }
408 ExprData::BigO(inner) => {
409 write_u8(w, 12)?;
410 write_u32(w, inner.0)
411 }
412 ExprData::RootSum { poly, var, body } => {
413 write_u8(w, 13)?;
414 write_u32(w, poly.0)?;
415 write_u32(w, var.0)?;
416 write_u32(w, body.0)
417 }
418 }
419}
420
421fn read_node(r: &mut Bounded<impl Read>, format_version: u32) -> Result<ExprData, IoError> {
422 let tag = read_u8(r)?;
423 match tag {
424 0 => {
425 let domain = u8_to_domain(read_u8(r)?)?;
426 let commutative = if format_version >= POOL_FORMAT_V4 {
427 read_u8(r)? != 0
428 } else {
429 true
430 };
431 let name = read_str(r)?;
432 Ok(ExprData::Symbol {
433 name,
434 domain,
435 commutative,
436 })
437 }
438 1 => {
439 let s = read_str(r)?;
440 let n: rug::Integer = s
441 .parse()
442 .map_err(|_| IoError::BadNumeric(format!("integer: {s}")))?;
443 Ok(ExprData::Integer(BigInt(n)))
444 }
445 2 => {
446 let nstr = read_str(r)?;
447 let dstr = read_str(r)?;
448 let n: rug::Integer = nstr
449 .parse()
450 .map_err(|_| IoError::BadNumeric(format!("numer: {nstr}")))?;
451 let d: rug::Integer = dstr
452 .parse()
453 .map_err(|_| IoError::BadNumeric(format!("denom: {dstr}")))?;
454 if d == 0 {
455 return Err(IoError::BadNumeric(format!("rational {nstr}/0")));
457 }
458 Ok(ExprData::Rational(BigRat(rug::Rational::from((n, d)))))
459 }
460 3 => {
461 let prec = read_u32(r)?;
462 if prec == 0 || prec > MAX_FILE_FLOAT_PREC {
463 return Err(IoError::BadNumeric(format!(
465 "float precision {prec} bits is outside 1..={MAX_FILE_FLOAT_PREC}"
466 )));
467 }
468 let s = read_str(r)?;
469 let f = rug::Float::parse_radix(&s, 16)
470 .map_err(|_| IoError::BadNumeric(format!("float: {s}")))?;
471 let inner = rug::Float::with_val(prec, f);
472 Ok(ExprData::Float(BigFloat { inner, prec }))
473 }
474 4 => Ok(ExprData::Add(read_ids(r)?)),
475 5 => Ok(ExprData::Mul(read_ids(r)?)),
476 6 => {
477 let base = ExprId(read_u32(r)?);
478 let exp = ExprId(read_u32(r)?);
479 Ok(ExprData::Pow { base, exp })
480 }
481 7 => {
482 let name = read_str(r)?;
483 let args = read_ids(r)?;
484 Ok(ExprData::Func { name, args })
485 }
486 8 => {
487 let n = u64::from(read_u32(r)?);
488 r.ensure(n, 8)?;
489 let mut branches = Vec::with_capacity(n as usize);
490 for _ in 0..n {
491 let c = ExprId(read_u32(r)?);
492 let v = ExprId(read_u32(r)?);
493 branches.push((c, v));
494 }
495 let default = ExprId(read_u32(r)?);
496 Ok(ExprData::Piecewise { branches, default })
497 }
498 9 => {
499 let kind = u8_to_pred(read_u8(r)?)?;
500 let args = read_ids(r)?;
501 Ok(ExprData::Predicate { kind, args })
502 }
503 10 => {
504 if format_version < POOL_FORMAT_V2 {
505 return Err(IoError::BadTag(10));
506 }
507 let var = ExprId(read_u32(r)?);
508 let body = ExprId(read_u32(r)?);
509 Ok(ExprData::Forall { var, body })
510 }
511 11 => {
512 if format_version < POOL_FORMAT_V2 {
513 return Err(IoError::BadTag(11));
514 }
515 let var = ExprId(read_u32(r)?);
516 let body = ExprId(read_u32(r)?);
517 Ok(ExprData::Exists { var, body })
518 }
519 12 => {
520 if format_version < POOL_FORMAT_V3 {
521 return Err(IoError::BadTag(12));
522 }
523 let inner = ExprId(read_u32(r)?);
524 Ok(ExprData::BigO(inner))
525 }
526 13 => {
527 if format_version < POOL_FORMAT_V5 {
528 return Err(IoError::BadTag(13));
529 }
530 let poly = ExprId(read_u32(r)?);
531 let var = ExprId(read_u32(r)?);
532 let body = ExprId(read_u32(r)?);
533 Ok(ExprData::RootSum { poly, var, body })
534 }
535 b => Err(IoError::BadTag(b)),
536 }
537}
538
539pub fn save_to(pool: &ExprPool, path: impl AsRef<Path>) -> Result<(), IoError> {
545 let path = path.as_ref();
546 let tmp: PathBuf = {
547 let mut p = path.to_path_buf();
548 let mut name = p
549 .file_name()
550 .map(|s| s.to_os_string())
551 .unwrap_or_else(|| std::ffi::OsString::from("pool"));
552 name.push(".tmp");
553 p.set_file_name(name);
554 p
555 };
556
557 {
558 let f = File::create(&tmp)?;
559 let mut w = BufWriter::new(f);
560
561 w.write_all(MAGIC)?;
562 write_u32(&mut w, POOL_FORMAT_WRITE)?;
563 write_u32(&mut w, 0u32)?; let count = pool.len();
566 write_u64(&mut w, count as u64)?;
567 for i in 0..count {
568 let data = pool.get(ExprId(i as u32));
569 write_node(&mut w, &data)?;
570 }
571
572 w.flush()?;
573 w.get_ref().sync_all()?;
574 }
575
576 fs::rename(&tmp, path)?;
577 Ok(())
578}
579
580pub fn load_from(path: impl AsRef<Path>) -> Result<Option<ExprPool>, IoError> {
583 let path = path.as_ref();
584 if !path.exists() {
585 return Ok(None);
586 }
587
588 let f = File::open(path)?;
589 let len = f.metadata()?.len();
590 let mut r = Bounded::new(BufReader::new(f), len);
591
592 let mut magic = [0u8; 4];
593 r.read_exact(&mut magic)?;
594 if &magic != MAGIC {
595 return Err(IoError::BadMagic);
596 }
597
598 let pool = ExprPool::new();
599 read_body(&mut r, &pool)?;
600 Ok(Some(pool))
601}
602
603fn read_body(r: &mut Bounded<impl Read>, pool: &ExprPool) -> Result<Vec<ExprId>, IoError> {
610 let version = read_u32(r)?;
611 if version != POOL_FORMAT_V1
612 && version != POOL_FORMAT_V2
613 && version != POOL_FORMAT_V3
614 && version != POOL_FORMAT_V4
615 && version != POOL_FORMAT_V5
616 {
617 return Err(IoError::UnsupportedVersion(version));
618 }
619 let _flags = read_u32(r)?;
620
621 let count = read_u64(r)?;
622 r.ensure(count, MIN_NODE_BYTES)?;
626 if count > u64::from(u32::MAX) {
627 return Err(IoError::Io(io::Error::new(
628 io::ErrorKind::InvalidData,
629 format!("pool file declares {count} nodes, more than an ExprId can address"),
630 )));
631 }
632 let count = count as usize;
633 let mut ids: Vec<ExprId> = Vec::new();
642 for index in 0..count {
643 let data = read_node(r, version)?;
644 let id = match remap_children(data, &ids, index)? {
649 ExprData::Add(args) => pool.add(args),
650 ExprData::Mul(args) => pool.mul(args),
651 other => pool.intern(other),
652 };
653 ids.push(id);
654 }
655 Ok(ids)
656}
657
658fn remap_children(data: ExprData, ids: &[ExprId], index: usize) -> Result<ExprData, IoError> {
666 map_children(data, |c| {
667 ids.get(c.0 as usize).copied().ok_or_else(|| {
668 IoError::Io(io::Error::new(
669 io::ErrorKind::InvalidData,
670 format!(
671 "pool file node {index} refers to node {}, which is not an earlier node",
672 c.0
673 ),
674 ))
675 })
676 })
677}
678
679fn map_children(
682 data: ExprData,
683 mut f: impl FnMut(ExprId) -> Result<ExprId, IoError>,
684) -> Result<ExprData, IoError> {
685 fn map_all(
686 v: Vec<ExprId>,
687 f: &mut impl FnMut(ExprId) -> Result<ExprId, IoError>,
688 ) -> Result<Vec<ExprId>, IoError> {
689 v.into_iter().map(f).collect()
690 }
691 Ok(match data {
692 ExprData::Symbol { .. }
693 | ExprData::Integer(_)
694 | ExprData::Rational(_)
695 | ExprData::Float(_) => data,
696 ExprData::Add(args) => ExprData::Add(map_all(args, &mut f)?),
697 ExprData::Mul(args) => ExprData::Mul(map_all(args, &mut f)?),
698 ExprData::Pow { base, exp } => ExprData::Pow {
699 base: f(base)?,
700 exp: f(exp)?,
701 },
702 ExprData::Func { name, args } => ExprData::Func {
703 name,
704 args: map_all(args, &mut f)?,
705 },
706 ExprData::Piecewise { branches, default } => ExprData::Piecewise {
707 branches: branches
708 .into_iter()
709 .map(|(c, v)| Ok((f(c)?, f(v)?)))
710 .collect::<Result<_, IoError>>()?,
711 default: f(default)?,
712 },
713 ExprData::Predicate { kind, args } => ExprData::Predicate {
714 kind,
715 args: map_all(args, &mut f)?,
716 },
717 ExprData::Forall { var, body } => ExprData::Forall {
718 var: f(var)?,
719 body: f(body)?,
720 },
721 ExprData::Exists { var, body } => ExprData::Exists {
722 var: f(var)?,
723 body: f(body)?,
724 },
725 ExprData::BigO(inner) => ExprData::BigO(f(inner)?),
726 ExprData::RootSum { poly, var, body } => ExprData::RootSum {
727 poly: f(poly)?,
728 var: f(var)?,
729 body: f(body)?,
730 },
731 })
732}
733
734const DAG_MAGIC: &[u8; 4] = b"ALKD";
740
741pub fn dag_to_bytes(pool: &ExprPool, roots: &[ExprId]) -> Vec<u8> {
753 use std::collections::HashMap;
754 let mut index: HashMap<ExprId, u32> = HashMap::new();
757 let mut order: Vec<ExprData> = Vec::new();
758 let mut stack: Vec<(ExprId, bool)> = roots.iter().rev().map(|&r| (r, false)).collect();
759 while let Some((id, expanded)) = stack.pop() {
760 if index.contains_key(&id) {
761 continue;
762 }
763 let data = pool.get(id);
764 if expanded {
765 let local = map_children(data, |c| Ok(ExprId(index[&c])))
766 .expect("every child was emitted before its parent");
767 index.insert(id, order.len() as u32);
768 order.push(local);
769 continue;
770 }
771 stack.push((id, true));
772 let mut children = Vec::new();
773 let _ = map_children(data, |c| {
774 children.push(c);
775 Ok(c)
776 });
777 for c in children.into_iter().rev() {
778 if !index.contains_key(&c) {
779 stack.push((c, false));
780 }
781 }
782 }
783 let mut out: Vec<u8> = Vec::new();
784 out.extend_from_slice(DAG_MAGIC);
785 out.extend_from_slice(&POOL_FORMAT_WRITE.to_le_bytes());
786 out.extend_from_slice(&0u32.to_le_bytes());
787 out.extend_from_slice(&(order.len() as u64).to_le_bytes());
788 for node in &order {
789 write_node(&mut out, node).expect("writing to a Vec cannot fail");
790 }
791 out.extend_from_slice(&(roots.len() as u32).to_le_bytes());
792 for r in roots {
793 out.extend_from_slice(&index[r].to_le_bytes());
794 }
795 out
796}
797
798pub fn dag_from_bytes(pool: &ExprPool, bytes: &[u8]) -> Result<Vec<ExprId>, IoError> {
806 let mut r = Bounded::new(bytes, bytes.len() as u64);
807 let mut magic = [0u8; 4];
808 r.read_exact(&mut magic)?;
809 if &magic != DAG_MAGIC {
810 return Err(IoError::BadMagic);
811 }
812 let ids = read_body(&mut r, pool)?;
813 let n_roots = u64::from(read_u32(&mut r)?);
814 r.ensure(n_roots, 4)?;
815 let mut roots = Vec::with_capacity(n_roots as usize);
816 for _ in 0..n_roots {
817 let i = read_u32(&mut r)?;
818 let id = ids.get(i as usize).copied().ok_or_else(|| {
819 IoError::Io(io::Error::new(
820 io::ErrorKind::InvalidData,
821 format!(
822 "expression DAG root {i} is not one of its {} nodes",
823 ids.len()
824 ),
825 ))
826 })?;
827 roots.push(id);
828 }
829 Ok(roots)
830}
831
832pub fn open_persistent(path: impl AsRef<Path>) -> Result<ExprPool, IoError> {
834 match load_from(path)? {
835 Some(p) => Ok(p),
836 None => Ok(ExprPool::new()),
837 }
838}
839
840impl ExprPool {
845 pub fn checkpoint(&self, path: impl AsRef<Path>) -> Result<(), IoError> {
848 save_to(self, path)
849 }
850
851 pub fn open_persistent(path: impl AsRef<Path>) -> Result<Self, IoError> {
854 open_persistent(path)
855 }
856}
857
858#[cfg(test)]
863mod tests {
864 use super::*;
865 use crate::kernel::{Domain, ExprData};
866
867 fn tempfile() -> PathBuf {
876 static SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
877 let mut p = std::env::temp_dir();
878 p.push(format!(
879 "alkahest_pool_{}_{}_{}.akp",
880 std::process::id(),
881 std::time::SystemTime::now()
882 .duration_since(std::time::UNIX_EPOCH)
883 .unwrap()
884 .as_nanos(),
885 SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
886 ));
887 p
888 }
889
890 #[test]
891 fn round_trip_small_pool() {
892 let p = ExprPool::new();
893 let x = p.symbol("x", Domain::Real);
894 let y = p.symbol("y", Domain::Positive);
895 let two = p.integer(2_i32);
896 let three_halves = p.rational(3, 2);
897 let f = p.float(1.5_f64, 53);
898 let xp = p.pow(x, two);
899 let fn_node = p.func("sin", vec![xp]);
900 let _sum = p.add(vec![fn_node, y, three_halves, f]);
901
902 let path = tempfile();
903 p.checkpoint(&path).unwrap();
904
905 let q = ExprPool::open_persistent(&path).unwrap();
906 assert_eq!(q.len(), p.len(), "node count must match");
907 for i in 0..p.len() {
908 let id = ExprId(i as u32);
909 assert_eq!(p.get(id), q.get(id), "node {i} mismatch after round-trip");
910 }
911
912 let q_x = q.symbol("x", Domain::Real);
915 assert_eq!(q_x, x, "symbol id drifted across checkpoint");
916 let q_two = q.integer(2_i32);
917 assert_eq!(q_two, two);
918
919 let _ = fs::remove_file(&path);
920 }
921
922 #[test]
923 fn round_trip_root_sum() {
924 let p = ExprPool::new();
926 let x = p.symbol("x", Domain::Real);
927 let t = p.symbol("t", Domain::Complex);
928 let poly = p.add(vec![p.pow(t, p.integer(2_i32)), p.integer(1_i32)]);
929 let body = p.mul(vec![t, p.func("log", vec![p.add(vec![x, t])])]);
930 let rs = p.root_sum(poly, t, body);
931 assert!(matches!(p.get(rs), ExprData::RootSum { .. }));
932
933 let path = tempfile();
934 p.checkpoint(&path).unwrap();
935 let q = ExprPool::open_persistent(&path).unwrap();
936 assert_eq!(q.len(), p.len());
937 for i in 0..p.len() {
938 let id = ExprId(i as u32);
939 assert_eq!(p.get(id), q.get(id), "node {i} mismatch after round-trip");
940 }
941 let _ = fs::remove_file(&path);
942 }
943
944 #[test]
945 fn bad_magic_rejected() {
946 let path = tempfile();
947 std::fs::write(&path, b"nope1234").unwrap();
948 match load_from(&path) {
949 Err(IoError::BadMagic) => {}
950 other => panic!("expected BadMagic, got {:?}", other.err()),
951 }
952 let _ = fs::remove_file(&path);
953 }
954
955 #[test]
956 fn missing_file_returns_fresh() {
957 let path = tempfile();
958 assert!(!path.exists());
959 let p = ExprPool::open_persistent(&path).unwrap();
960 assert_eq!(p.len(), 0);
961 }
962
963 #[test]
964 fn predicate_and_piecewise_round_trip() {
965 let p = ExprPool::new();
966 let x = p.symbol("x", Domain::Real);
967 let zero = p.integer(0_i32);
968 let one = p.integer(1_i32);
969 let neg_one = p.integer(-1_i32);
970 let cond = p.intern(ExprData::Predicate {
971 kind: PredicateKind::Gt,
972 args: vec![x, zero],
973 });
974 let pc = p.intern(ExprData::Piecewise {
975 branches: vec![(cond, one)],
976 default: neg_one,
977 });
978
979 let path = tempfile();
980 p.checkpoint(&path).unwrap();
981 let q = ExprPool::open_persistent(&path).unwrap();
982 assert_eq!(p.get(pc), q.get(pc));
983 let _ = fs::remove_file(&path);
984 }
985
986 fn write_raw(path: &Path, nodes: &[ExprData]) {
989 let mut w = BufWriter::new(File::create(path).unwrap());
990 w.write_all(MAGIC).unwrap();
991 write_u32(&mut w, POOL_FORMAT_WRITE).unwrap();
992 write_u32(&mut w, 0).unwrap();
993 write_u64(&mut w, nodes.len() as u64).unwrap();
994 for n in nodes {
995 write_node(&mut w, n).unwrap();
996 }
997 w.flush().unwrap();
998 }
999
1000 #[test]
1006 fn non_canonical_nodes_collapse_without_drift() {
1007 let x = ExprData::Symbol {
1008 name: "x".into(),
1009 domain: Domain::Real,
1010 commutative: true,
1011 };
1012 let y = ExprData::Symbol {
1013 name: "y".into(),
1014 domain: Domain::Real,
1015 commutative: true,
1016 };
1017 let nodes = vec![
1018 x, ExprData::Rational(BigRat(rug::Rational::from((4, 2)))), ExprData::Integer(BigInt(rug::Integer::from(2))), ExprData::Add(vec![ExprId(0)]), y, ExprData::Add(vec![ExprId(4), ExprId(3)]), ExprData::Pow {
1025 base: ExprId(5),
1026 exp: ExprId(2),
1027 }, ExprData::Mul(vec![]), ExprData::Pow {
1030 base: ExprId(4),
1031 exp: ExprId(7),
1032 }, ];
1034 let path = tempfile();
1035 write_raw(&path, &nodes);
1036 let q = load_from(&path).unwrap().unwrap();
1037 let _ = fs::remove_file(&path);
1038
1039 let x = q.symbol("x", Domain::Real);
1040 let y = q.symbol("y", Domain::Real);
1041 let two = q.integer(2_i32);
1042 let sum = q.add(vec![x, y]);
1043 let square = q.pow(sum, two);
1044 let y_one = q.pow(y, q.integer(1_i32));
1045 assert_eq!(q.len(), 7, "a non-canonical node survived as a duplicate");
1047 for id in [x, y, two, sum, square, y_one] {
1048 assert!(
1049 (id.0 as usize) < 7,
1050 "{} was not in the loaded pool",
1051 q.display(id)
1052 );
1053 }
1054 }
1055
1056 #[test]
1057 fn a_forward_child_reference_is_refused() {
1058 let nodes = vec![ExprData::Add(vec![ExprId(0), ExprId(1)])];
1059 let path = tempfile();
1060 write_raw(&path, &nodes);
1061 let r = load_from(&path);
1062 let _ = fs::remove_file(&path);
1063 assert!(
1064 r.is_err(),
1065 "a child reference past the node itself must be refused"
1066 );
1067 }
1068
1069 #[test]
1070 fn big_o_round_trip() {
1071 let p = ExprPool::new();
1072 let x = p.symbol("x", Domain::Real);
1073 let o = p.big_o(p.pow(x, p.integer(6)));
1074 let path = tempfile();
1075 p.checkpoint(&path).unwrap();
1076 let q = ExprPool::open_persistent(&path).unwrap();
1077 assert_eq!(q.get(o), p.get(o));
1078 let _ = fs::remove_file(&path);
1079 }
1080
1081 fn header(count: u64) -> Vec<u8> {
1084 let mut v = MAGIC.to_vec();
1085 v.extend_from_slice(&POOL_FORMAT_WRITE.to_le_bytes());
1086 v.extend_from_slice(&0u32.to_le_bytes());
1087 v.extend_from_slice(&count.to_le_bytes());
1088 v
1089 }
1090
1091 fn load_bytes(bytes: &[u8]) -> Result<Option<ExprPool>, IoError> {
1092 let path = tempfile();
1093 std::fs::write(&path, bytes).unwrap();
1094 let r = load_from(&path);
1095 let _ = fs::remove_file(&path);
1096 r
1097 }
1098
1099 fn le32(v: u32) -> [u8; 4] {
1100 v.to_le_bytes()
1101 }
1102
1103 #[test]
1107 fn hostile_headers_are_refused_not_allocated() {
1108 let mut cases: Vec<(&str, Vec<u8>, &str)> = Vec::new();
1109 let mut b = header(1);
1111 b.extend_from_slice(&[0, 0, 1]);
1112 b.extend_from_slice(&le32(0xFFFF_FFF0));
1113 cases.push(("huge string length", b, "E-IO-004"));
1114 let mut b = header(1);
1116 b.push(4);
1117 b.extend_from_slice(&le32(0xFFFF_FFF0));
1118 cases.push(("huge arity", b, "E-IO-004"));
1119 let mut b = header(1);
1121 b.push(8);
1122 b.extend_from_slice(&le32(0xFFFF_FFF0));
1123 cases.push(("huge branch count", b, "E-IO-004"));
1124 let mut b = header(1 << 62);
1126 b.extend_from_slice(&[0, 0, 1]);
1127 b.extend_from_slice(&le32(1));
1128 b.push(b'x');
1129 cases.push(("huge node count", b, "E-IO-004"));
1130 let mut b = header(1);
1132 b.push(2);
1133 b.extend_from_slice(&le32(1));
1134 b.push(b'1');
1135 b.extend_from_slice(&le32(1));
1136 b.push(b'0');
1137 cases.push(("zero denominator", b, "E-IO-009"));
1138 for prec in [0u32, u32::MAX, MAX_FILE_FLOAT_PREC + 1] {
1140 let mut b = header(1);
1141 b.push(3);
1142 b.extend_from_slice(&le32(prec));
1143 b.extend_from_slice(&le32(1));
1144 b.push(b'1');
1145 cases.push(("float precision", b, "E-IO-009"));
1146 }
1147 use crate::errors::AlkahestError;
1148 for (what, bytes, code) in cases {
1149 let r = std::panic::catch_unwind(|| load_bytes(&bytes))
1150 .unwrap_or_else(|_| panic!("{what}: loading panicked"));
1151 match r {
1152 Err(e) => assert_eq!(e.code(), code, "{what}: {e}"),
1153 Ok(_) => panic!("{what}: a corrupt file loaded"),
1154 }
1155 }
1156 }
1157
1158 #[test]
1161 fn duplicate_nodes_do_not_drift_later_references() {
1162 let sym = |n: &str| ExprData::Symbol {
1163 name: n.into(),
1164 domain: Domain::Real,
1165 commutative: true,
1166 };
1167 let nodes = vec![
1168 sym("x"),
1169 sym("x"),
1170 sym("y"),
1171 ExprData::Pow {
1172 base: ExprId(1),
1173 exp: ExprId(2),
1174 },
1175 ];
1176 let path = tempfile();
1177 write_raw(&path, &nodes);
1178 let q = load_from(&path).unwrap().unwrap();
1179 let _ = fs::remove_file(&path);
1180 let x = q.symbol("x", Domain::Real);
1181 let y = q.symbol("y", Domain::Real);
1182 let before = q.len();
1183 let _ = q.pow(x, y);
1184 assert_eq!(q.len(), before, "x^y was not the node the file meant");
1185 assert_eq!(q.len(), 3);
1186 }
1187
1188 fn every_tag_file() -> Vec<u8> {
1190 let p = ExprPool::new();
1191 let x = p.symbol("x", Domain::Real);
1192 let t = p.symbol("t", Domain::Complex);
1193 let big = p.integer(rug::Integer::from(rug::Integer::u_pow_u(10, 30)));
1194 let r = p.rational(-3, 7);
1195 let f = p.float(2.5, 80);
1196 let s = p.add(vec![x, big, r, f]);
1197 let m = p.mul(vec![x, t]);
1198 let pw = p.pow(s, m);
1199 let sn = p.func("sin", vec![pw]);
1200 let g = p.func("f", vec![x, t, sn]);
1201 let c = p.pred_gt(x, p.integer(0));
1202 let pc = p.piecewise(vec![(c, g)], r);
1203 let fa = p.forall(x, c);
1204 let ex = p.exists(t, fa);
1205 let o = p.big_o(p.pow(x, p.integer(3)));
1206 let poly = p.add(vec![p.pow(t, p.integer(2)), p.integer(1)]);
1207 let rs = p.root_sum(poly, t, p.func("log", vec![p.add(vec![x, t])]));
1208 let _ = p.add(vec![pc, ex, o, rs]);
1209 let path = tempfile();
1210 p.checkpoint(&path).unwrap();
1211 let bytes = std::fs::read(&path).unwrap();
1212 let _ = fs::remove_file(&path);
1213 bytes
1214 }
1215
1216 fn load_and_touch(bytes: &[u8]) -> bool {
1219 std::panic::catch_unwind(|| {
1220 if let Ok(Some(q)) = load_bytes(bytes) {
1221 for i in 0..q.len() {
1222 let _ = q.display(ExprId(i as u32)).to_string();
1223 }
1224 }
1225 })
1226 .is_ok()
1227 }
1228
1229 #[test]
1233 fn truncated_and_bit_flipped_files_never_panic() {
1234 let good = every_tag_file();
1235 assert!(load_bytes(&good).unwrap().is_some());
1236 let mut bad = Vec::new();
1237 for n in 0..good.len() {
1238 if !load_and_touch(&good[..n]) {
1239 bad.push(format!("truncated to {n}"));
1240 }
1241 }
1242 for i in 0..good.len() {
1243 for bit in 0..8 {
1244 let mut b = good.clone();
1245 b[i] ^= 1 << bit;
1246 if !load_and_touch(&b) {
1247 bad.push(format!("byte {i} bit {bit}"));
1248 }
1249 }
1250 }
1251 assert!(bad.is_empty(), "panicked on: {bad:?}");
1252 }
1253
1254 fn show(pool: &ExprPool, id: ExprId) -> String {
1258 pool.display(id).to_string()
1259 }
1260
1261 #[test]
1262 fn dag_round_trips_into_a_fresh_pool_and_carries_only_what_is_reachable() {
1263 let p = ExprPool::new();
1264 let x = p.symbol("x", Domain::Real);
1265 let y = p.symbol("y", Domain::Positive);
1266 for i in 0..200 {
1268 let _ = p.add(vec![x, p.integer(i)]);
1269 }
1270 let s = p.add(vec![x, y]);
1271 let e1 = p.pow(s, p.rational(3, 7));
1272 let e2 = p.func("sin", vec![p.mul(vec![s, p.float(0.25, 80)])]);
1273 let bytes = dag_to_bytes(&p, &[e1, e2, e1]);
1274 assert!(
1275 bytes.len() < 400,
1276 "{} bytes for two small expressions",
1277 bytes.len()
1278 );
1279
1280 let q = ExprPool::new();
1281 let got = dag_from_bytes(&q, &bytes).unwrap();
1282 assert_eq!(got.len(), 3);
1283 assert_eq!(got[0], got[2]);
1284 assert_eq!(show(&q, got[0]), show(&p, e1));
1285 assert_eq!(show(&q, got[1]), show(&p, e2));
1286 assert_eq!(q.get(q.symbol("y", Domain::Positive)), p.get(y));
1288 let qy = q.symbol("y", Domain::Positive);
1289 let qx = q.symbol("x", Domain::Real);
1290 assert_eq!(q.add(vec![qx, qy]), q.add(vec![qy, qx]));
1291 let before = q.len();
1292 let _ = q.add(vec![qx, qy]);
1293 assert_eq!(q.len(), before, "the shared x + y was not reused");
1294 }
1295
1296 #[test]
1297 fn dag_loaded_into_its_own_pool_returns_the_original_ids() {
1298 let p = ExprPool::new();
1299 let x = p.symbol("x", Domain::Real);
1300 let e = p.add(vec![p.pow(x, p.integer(2)), p.func("exp", vec![x])]);
1301 let before = p.len();
1302 let got = dag_from_bytes(&p, &dag_to_bytes(&p, &[e])).unwrap();
1303 assert_eq!(got, vec![e]);
1304 assert_eq!(p.len(), before);
1305 }
1306
1307 #[test]
1308 fn dag_of_a_very_deep_expression_serialises_iteratively() {
1309 let p = ExprPool::new();
1310 let mut e = p.symbol("x", Domain::Real);
1311 for _ in 0..200_000 {
1312 e = p.func("sin", vec![e]);
1313 }
1314 let bytes = dag_to_bytes(&p, &[e]);
1315 let q = ExprPool::new();
1316 let got = dag_from_bytes(&q, &bytes).unwrap();
1317 assert_eq!(q.len(), p.len());
1318 assert!(matches!(q.get(got[0]), ExprData::Func { .. }));
1319 }
1320
1321 #[test]
1322 fn corrupt_dag_bytes_are_errors_not_panics() {
1323 let p = ExprPool::new();
1324 let x = p.symbol("x", Domain::Real);
1325 let e = p.add(vec![p.pow(x, p.rational(1, 3)), p.float(1.5, 64), x]);
1326 let good = dag_to_bytes(&p, &[e]);
1327 let mut bad = Vec::new();
1328 let try_load = |b: &[u8]| {
1329 std::panic::catch_unwind(|| {
1330 let q = ExprPool::new();
1331 if let Ok(roots) = dag_from_bytes(&q, b) {
1332 for r in roots {
1333 let _ = q.display(r).to_string();
1334 }
1335 }
1336 })
1337 .is_ok()
1338 };
1339 for n in 0..good.len() {
1340 if !try_load(&good[..n]) {
1341 bad.push(format!("truncated to {n}"));
1342 }
1343 }
1344 for i in 0..good.len() {
1345 for bit in 0..8 {
1346 let mut b = good.clone();
1347 b[i] ^= 1 << bit;
1348 if !try_load(&b) {
1349 bad.push(format!("byte {i} bit {bit}"));
1350 }
1351 }
1352 }
1353 assert!(bad.is_empty(), "panicked on: {bad:?}");
1354 assert!(matches!(
1356 dag_from_bytes(&p, b"ALKP"),
1357 Err(IoError::BadMagic)
1358 ));
1359 }
1360}