use std::{error::Error, fmt, mem::size_of};
use monty_types::TypeCheckState;
use serde::{Deserialize, Serialize};
use crate::{
repl::{MontyRepl, ReplProgress},
run_progress::RunProgress,
};
const MAGIC: &[u8; 6] = b"MONTY\0";
pub const DUMP_VERSION: u16 = 5;
const HEADER_LEN: usize = MAGIC.len() + size_of::<u16>();
pub fn dump(
script_name: &str,
type_check: Option<&TypeCheckState>,
state: SessionRef<'_>,
) -> Result<Vec<u8>, postcard::Error> {
#[derive(Serialize)]
struct DumpRef<'a> {
script_name: &'a str,
type_check: Option<&'a TypeCheckState>,
state: SessionRef<'a>,
}
let payload = postcard::to_allocvec(&DumpRef {
script_name,
type_check,
state,
})?;
let mut bytes = Vec::with_capacity(HEADER_LEN + payload.len());
bytes.extend_from_slice(MAGIC);
bytes.extend_from_slice(&DUMP_VERSION.to_le_bytes());
bytes.extend_from_slice(&payload);
Ok(bytes)
}
#[derive(Debug, Deserialize)]
pub struct Dump {
pub script_name: String,
pub type_check: Option<TypeCheckState>,
pub state: Session,
}
impl Dump {
pub fn load(bytes: &[u8]) -> Result<Self, DumpError> {
let Some(header) = bytes.get(..HEADER_LEN) else {
return Err(DumpError::NotADump);
};
let version = u16::from_le_bytes([header[MAGIC.len()], header[MAGIC.len() + 1]]);
if &header[..MAGIC.len()] != MAGIC {
Err(DumpError::NotADump)
} else if version != DUMP_VERSION {
Err(DumpError::VersionMismatch {
found: version,
expected: DUMP_VERSION,
})
} else {
let (value, remainder) = postcard::take_from_bytes(&bytes[HEADER_LEN..]).map_err(DumpError::Payload)?;
if remainder.is_empty() {
Ok(value)
} else {
Err(DumpError::Payload(postcard::Error::DeserializeBadEncoding))
}
}
}
}
#[derive(Debug, Deserialize)]
pub enum Session {
Idle(Box<MontyRepl>),
Suspended(Box<ReplProgress>),
Running(Box<RunProgress>),
}
#[derive(Debug, Serialize)]
pub enum SessionRef<'a> {
Idle(&'a MontyRepl),
Suspended(&'a ReplProgress),
Running(&'a RunProgress),
}
#[derive(Debug, PartialEq, Eq)]
pub enum DumpError {
NotADump,
VersionMismatch {
found: u16,
expected: u16,
},
Payload(postcard::Error),
}
impl fmt::Display for DumpError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotADump => write!(f, "not a monty dump"),
Self::VersionMismatch { found, expected } => {
write!(f, "dump format version {found}, this build reads {expected}")
}
Self::Payload(err) => write!(f, "malformed dump payload: {err}"),
}
}
}
impl Error for DumpError {}
#[cfg(test)]
mod tests {
use monty_types::TypeCheckingFormat;
use strum::VariantNames;
use super::DUMP_VERSION;
use crate::{
bytecode::opcode_fingerprint, expressions::comparison_operators_fingerprint, intern::static_strings_fingerprint,
};
#[test]
fn serialized_components_match_dump_version() {
assert_eq!(
opcode_fingerprint(),
0x0d57_34dd_be07_19ac,
"opcodes changed for dump version {DUMP_VERSION}"
);
assert_eq!(
static_strings_fingerprint(),
0x239c_c721_30be_eba3,
"static strings changed for dump version {DUMP_VERSION}"
);
assert_eq!(
comparison_operators_fingerprint(),
0x8ecc_d26b_160d_9c0b,
"comparison operators changed for dump version {DUMP_VERSION}"
);
}
#[test]
fn type_checking_format_variants_match_dump_version() {
assert_eq!(
TypeCheckingFormat::VARIANTS,
[
"full",
"concise",
"azure",
"json",
"jsonlines",
"rdjson",
"pylint",
"gitlab",
"github"
],
"TypeCheckingFormat variants changed for dump version {DUMP_VERSION}"
);
}
}