use std::fs::{File, OpenOptions};
use std::io::{self, BufWriter, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use crate::proof::{ProofNodeId, ProofStep};
const MAGIC: &[u8; 8] = b"OXIZPROF";
const FORMAT_VERSION: u32 = 1;
const KIND_AXIOM: u8 = 0;
const KIND_INFERENCE: u8 = 1;
#[derive(Error, Debug)]
pub enum LoggingError {
#[error("I/O error while logging proof: {0}")]
Io(#[from] io::Error),
#[error("attempted to write to a closed ProofLogger")]
AlreadyClosed,
}
pub type LoggingResult<T> = Result<T, LoggingError>;
pub struct ProofLogger {
writer: Option<BufWriter<File>>,
path: PathBuf,
step_count: u64,
}
impl ProofLogger {
pub fn create(path: &Path) -> LoggingResult<Self> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
write_header(&mut writer)?;
writer.flush()?;
Ok(Self {
writer: Some(writer),
path: path.to_path_buf(),
step_count: 0,
})
}
pub fn append(path: &Path) -> LoggingResult<Self> {
let file = OpenOptions::new().append(true).open(path)?;
let writer = BufWriter::new(file);
Ok(Self {
writer: Some(writer),
path: path.to_path_buf(),
step_count: 0,
})
}
pub fn log_step(&mut self, node_id: ProofNodeId, step: &ProofStep) -> LoggingResult<()> {
let writer = self.writer.as_mut().ok_or(LoggingError::AlreadyClosed)?;
write_step(writer, node_id, step)?;
self.step_count += 1;
Ok(())
}
pub fn flush(&mut self) -> LoggingResult<()> {
if let Some(ref mut w) = self.writer {
w.flush()?;
}
Ok(())
}
pub fn close(&mut self) -> LoggingResult<()> {
if let Some(mut writer) = self.writer.take() {
writer.flush()?;
}
Ok(())
}
#[must_use]
pub fn step_count(&self) -> u64 {
self.step_count
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.writer.is_none()
}
}
impl Drop for ProofLogger {
fn drop(&mut self) {
if let Some(ref mut w) = self.writer {
let _ = w.flush();
}
}
}
fn write_header<W: Write>(w: &mut W) -> io::Result<()> {
w.write_all(MAGIC)?;
w.write_all(&FORMAT_VERSION.to_le_bytes())?;
w.write_all(&0u32.to_le_bytes())?;
w.write_all(&[0u8; 16])?;
Ok(())
}
fn write_step<W: Write>(w: &mut W, node_id: ProofNodeId, step: &ProofStep) -> io::Result<()> {
match step {
ProofStep::Axiom { conclusion } => {
w.write_all(&[KIND_AXIOM])?;
w.write_all(&node_id.0.to_le_bytes())?;
write_bytes(w, conclusion.as_bytes())?;
}
ProofStep::Inference {
rule,
premises,
conclusion,
args,
} => {
w.write_all(&[KIND_INFERENCE])?;
w.write_all(&node_id.0.to_le_bytes())?;
write_bytes(w, conclusion.as_bytes())?;
write_bytes(w, rule.as_bytes())?;
let premise_count = premises.len() as u32;
w.write_all(&premise_count.to_le_bytes())?;
for p in premises.iter() {
w.write_all(&p.0.to_le_bytes())?;
}
let arg_count = args.len() as u32;
w.write_all(&arg_count.to_le_bytes())?;
for arg in args.iter() {
write_bytes(w, arg.as_bytes())?;
}
}
}
Ok(())
}
fn write_bytes<W: Write>(w: &mut W, data: &[u8]) -> io::Result<()> {
let len = data.len() as u32;
w.write_all(&len.to_le_bytes())?;
w.write_all(data)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proof::{ProofNodeId, ProofStep};
use smallvec::SmallVec;
fn temp_log_path(name: &str) -> PathBuf {
std::env::temp_dir().join(format!("oxiz_proof_log_{}.oxizlog", name))
}
#[test]
fn test_create_and_close() {
let path = temp_log_path("create_close");
let _ = std::fs::remove_file(&path);
let mut logger = ProofLogger::create(&path).expect("create failed");
assert!(!logger.is_closed());
logger.close().expect("close failed");
assert!(logger.is_closed());
assert!(path.exists());
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_log_axiom() {
let path = temp_log_path("log_axiom");
let _ = std::fs::remove_file(&path);
let mut logger = ProofLogger::create(&path).expect("create failed");
let step = ProofStep::Axiom {
conclusion: "(assert (= x 0))".to_string(),
};
logger.log_step(ProofNodeId(0), &step).expect("log failed");
logger.close().expect("close failed");
assert!(path.metadata().expect("stat failed").len() > 32);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_log_inference() {
let path = temp_log_path("log_inference");
let _ = std::fs::remove_file(&path);
let mut logger = ProofLogger::create(&path).expect("create failed");
let axiom = ProofStep::Axiom {
conclusion: "p".to_string(),
};
logger.log_step(ProofNodeId(0), &axiom).expect("log failed");
let inference = ProofStep::Inference {
rule: "mp".to_string(),
premises: SmallVec::from_vec(vec![ProofNodeId(0)]),
conclusion: "q".to_string(),
args: SmallVec::new(),
};
logger
.log_step(ProofNodeId(1), &inference)
.expect("log failed");
assert_eq!(logger.step_count(), 2);
logger.close().expect("close failed");
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_write_after_close_fails() {
let path = temp_log_path("after_close");
let _ = std::fs::remove_file(&path);
let mut logger = ProofLogger::create(&path).expect("create failed");
logger.close().expect("close failed");
let step = ProofStep::Axiom {
conclusion: "p".to_string(),
};
let result = logger.log_step(ProofNodeId(0), &step);
assert!(matches!(result, Err(LoggingError::AlreadyClosed)));
let _ = std::fs::remove_file(&path);
}
}