pub mod convert;
pub mod runtime;
pub mod types;
use std::io::{Read, Write};
use std::path::Path;
use sha2::{Digest, Sha256};
use self::convert::*;
use self::runtime::*;
use self::types::DumpContextState;
use crate::emacs_core::charset::{
CharsetRegistrySnapshot, restore_charset_registry, snapshot_charset_registry,
};
use crate::emacs_core::eval::Context;
use crate::emacs_core::fontset::{
FontsetRegistrySnapshot, restore_fontset_registry, snapshot_fontset_registry,
};
use crate::emacs_core::intern;
use crate::emacs_core::value;
const MAGIC: &[u8; 8] = b"NEOPDUMP";
const FORMAT_VERSION: u32 = 10;
#[derive(Debug)]
pub enum DumpError {
Io(std::io::Error),
BadMagic,
UnsupportedVersion(u32),
ChecksumMismatch,
SerializationError(String),
DeserializationError(String),
}
impl std::fmt::Display for DumpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DumpError::Io(e) => write!(f, "I/O error: {e}"),
DumpError::BadMagic => write!(f, "not a valid pdump file (bad magic)"),
DumpError::UnsupportedVersion(v) => write!(f, "unsupported pdump version {v}"),
DumpError::ChecksumMismatch => write!(f, "pdump checksum mismatch (corrupted file)"),
DumpError::SerializationError(s) => write!(f, "serialization error: {s}"),
DumpError::DeserializationError(s) => write!(f, "deserialization error: {s}"),
}
}
}
impl std::error::Error for DumpError {}
impl From<std::io::Error> for DumpError {
fn from(e: std::io::Error) -> Self {
DumpError::Io(e)
}
}
#[derive(Clone, Debug)]
pub struct ActiveRuntimeSnapshot {
charset_registry: CharsetRegistrySnapshot,
fontset_registry: FontsetRegistrySnapshot,
}
pub fn dump_to_file(eval: &Context, path: &Path) -> Result<(), DumpError> {
let state = dump_evaluator(eval);
let payload =
bincode::serialize(&state).map_err(|e| DumpError::SerializationError(e.to_string()))?;
let mut hasher = Sha256::new();
hasher.update(&payload);
let checksum = hasher.finalize();
let parent = path.parent().unwrap_or_else(|| Path::new("."));
let mut file = tempfile::NamedTempFile::new_in(parent)?;
file.write_all(MAGIC)?;
file.write_all(&FORMAT_VERSION.to_le_bytes())?;
file.write_all(&checksum)?;
file.write_all(&(payload.len() as u32).to_le_bytes())?;
file.write_all(&payload)?;
file.flush()?;
file.as_file().sync_all()?;
match file.persist(path) {
Ok(_) => Ok(()),
Err(err) => {
if err.error.kind() == std::io::ErrorKind::AlreadyExists && path.exists() {
Ok(())
} else {
Err(DumpError::Io(err.error))
}
}
}
}
pub fn load_from_dump(path: &Path) -> Result<Context, DumpError> {
let load_start = std::time::Instant::now();
let mut file = std::fs::File::open(path)?;
let mut magic = [0u8; 8];
file.read_exact(&mut magic)?;
if &magic != MAGIC {
return Err(DumpError::BadMagic);
}
let mut version_bytes = [0u8; 4];
file.read_exact(&mut version_bytes)?;
let version = u32::from_le_bytes(version_bytes);
if version != FORMAT_VERSION {
return Err(DumpError::UnsupportedVersion(version));
}
let mut expected_checksum = [0u8; 32];
file.read_exact(&mut expected_checksum)?;
let mut len_bytes = [0u8; 4];
file.read_exact(&mut len_bytes)?;
let payload_len = u32::from_le_bytes(len_bytes) as usize;
let mut payload = vec![0u8; payload_len];
file.read_exact(&mut payload)?;
let mut hasher = Sha256::new();
hasher.update(&payload);
let actual_checksum = hasher.finalize();
if actual_checksum.as_slice() != &expected_checksum {
return Err(DumpError::ChecksumMismatch);
}
let state: types::DumpContextState = bincode::deserialize(&payload)
.map_err(|e| DumpError::DeserializationError(e.to_string()))?;
let mut eval = reconstruct_evaluator(&state)?;
record_loaded_dump(path, load_start.elapsed());
run_after_pdump_load_hook(&mut eval);
Ok(eval)
}
pub fn snapshot_evaluator(eval: &Context) -> DumpContextState {
dump_evaluator(eval)
}
pub fn snapshot_active_evaluator(eval: &mut Context) -> DumpContextState {
eval.setup_thread_locals();
dump_evaluator(eval)
}
pub fn snapshot_active_runtime(eval: &mut Context) -> ActiveRuntimeSnapshot {
eval.setup_thread_locals();
ActiveRuntimeSnapshot {
charset_registry: snapshot_charset_registry(),
fontset_registry: snapshot_fontset_registry(),
}
}
pub fn restore_active_runtime(eval: &mut Context, snapshot: &ActiveRuntimeSnapshot) {
eval.setup_thread_locals();
restore_charset_registry(snapshot.charset_registry.clone());
restore_fontset_registry(snapshot.fontset_registry.clone());
eval.sync_thread_runtime_bindings();
eval.sync_current_thread_buffer_state();
}
pub fn restore_snapshot(state: &DumpContextState) -> Result<Context, DumpError> {
reconstruct_evaluator(state)
}
pub fn clone_evaluator(eval: &Context) -> Result<Context, DumpError> {
restore_snapshot(&snapshot_evaluator(eval))
}
pub fn clone_active_evaluator(eval: &mut Context) -> Result<Context, DumpError> {
restore_snapshot(&snapshot_active_evaluator(eval))
}
fn reconstruct_evaluator(state: &DumpContextState) -> Result<Context, DumpError> {
load_interner(&state.interner);
let mut tagged_heap = Box::new(crate::tagged::gc::TaggedHeap::new());
crate::tagged::gc::set_tagged_heap(&mut tagged_heap);
preload_tagged_heap(&state.tagged_heap)?;
reset_runtime_for_new_heap(HeapResetMode::PdumpRestore);
load_charset_registry(&state.charset_registry);
load_fontset_registry(&state.fontset_registry);
let obarray = load_obarray(&state.obarray);
let lexenv = load_value(&state.lexenv);
let features: Vec<_> = state.features.iter().map(|id| intern::SymId(*id)).collect();
let require_stack: Vec<_> = state
.require_stack
.iter()
.map(|id| intern::SymId(*id))
.collect();
let loads_in_progress: Vec<_> = state
.loads_in_progress
.iter()
.map(std::path::PathBuf::from)
.collect();
let eval = Context::from_dump(
tagged_heap,
obarray,
lexenv,
features,
require_stack,
loads_in_progress,
load_buffer_manager(&state.buffers),
load_autoload_manager(&state.autoloads),
load_custom_manager(&state.custom),
load_mode_registry(&state.modes),
load_coding_system_manager(&state.coding_systems),
load_face_table(&state.face_table),
load_abbrev_manager(&state.abbrevs),
load_interactive_registry(&state.interactive),
load_rectangle(&state.rectangle),
load_value(&state.standard_syntax_table),
load_value(&state.standard_category_table),
load_value(&state.current_local_map),
load_kmacro(&state.kmacro),
load_register_manager(&state.registers),
load_bookmark_manager(&state.bookmarks),
load_watcher_list(&state.watchers),
);
finish_preload_tagged_heap();
Ok(eval)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::emacs_core::intern::intern;
use crate::emacs_core::pdump::types::{
DumpByteCodeFunction, DumpHeapObject, DumpLambdaParams, DumpOp,
};
use crate::emacs_core::value::Value;
#[test]
fn test_pdump_round_trip_basic() {
crate::test_utils::init_test_tracing();
let mut eval = Context::new();
eval.obarray
.set_symbol_value("test-pdump-var", Value::fixnum(42));
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("test.pdump");
dump_to_file(&eval, &dump_path).expect("dump should succeed");
let loaded = load_from_dump(&dump_path).expect("load should succeed");
assert_eq!(
loaded.obarray.symbol_value("test-pdump-var"),
Some(&Value::fixnum(42))
);
}
#[test]
fn test_clone_active_evaluator_preserves_in_progress_require_and_load_state() {
crate::test_utils::init_test_tracing();
let mut eval = Context::new();
eval.require_stack.push(intern("cl-macs"));
eval.loads_in_progress.push(std::path::PathBuf::from(
"/tmp/neomacs-pdump-clone-in-progress.el",
));
let cloned = clone_active_evaluator(&mut eval).expect("clone should succeed");
assert_eq!(cloned.require_stack, vec![intern("cl-macs")]);
assert_eq!(
cloned.loads_in_progress,
vec![std::path::PathBuf::from(
"/tmp/neomacs-pdump-clone-in-progress.el"
)]
);
}
#[test]
fn test_restore_active_runtime_after_clone_reinstalls_live_charset_registry() {
crate::test_utils::init_test_tracing();
crate::emacs_core::charset::reset_charset_registry();
let mut eval = Context::new();
let mut args = vec![value::Value::NIL; 17];
args[0] = value::Value::symbol("charset-pdump-clone-restore-test");
args[1] = value::Value::fixnum(1);
args[2] = value::Value::vector(vec![value::Value::fixnum(0), value::Value::fixnum(127)]);
args[16] = value::Value::list(vec![
value::Value::symbol("doc"),
value::Value::string("live charset registry should survive clone handoff"),
]);
crate::emacs_core::charset::builtin_define_charset_internal(args).unwrap();
let live_runtime = snapshot_active_runtime(&mut eval);
let cloned = clone_active_evaluator(&mut eval).expect("first clone should succeed");
restore_active_runtime(&mut eval, &live_runtime);
drop(cloned);
let cloned_again = clone_active_evaluator(&mut eval).expect("second clone should succeed");
restore_active_runtime(&mut eval, &live_runtime);
drop(cloned_again);
let registry = crate::emacs_core::charset::snapshot_charset_registry();
let entry = registry
.charsets
.iter()
.find(|info| info.name == "charset-pdump-clone-restore-test")
.expect("restored charset entry");
assert_eq!(
entry.plist,
vec![(
"doc".to_string(),
value::Value::string("live charset registry should survive clone handoff"),
)]
);
}
#[test]
fn test_file_load_records_pdumper_stats_and_runs_after_pdump_load_hook() {
crate::test_utils::init_test_tracing();
let mut eval = Context::new();
let setup = crate::emacs_core::value_reader::read_all(
"(progn
(setq compat-pdump-hook-fired nil)
(setq after-pdump-load-hook
(list (lambda () (setq compat-pdump-hook-fired t)))))",
)
.unwrap();
eval.eval_sub(setup[0])
.expect("setup hook should evaluate");
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("stats-and-hook.pdump");
dump_to_file(&eval, &dump_path).expect("dump should succeed");
drop(eval);
let mut loaded = load_from_dump(&dump_path).expect("load should succeed");
assert_eq!(
loaded.obarray.symbol_value("compat-pdump-hook-fired"),
Some(&Value::T)
);
let forms = crate::emacs_core::value_reader::read_all("(pdumper-stats)").unwrap();
let stats = loaded
.eval_sub(forms[0])
.expect("pdumper-stats should evaluate");
assert!(stats.is_cons(), "pdumper-stats should return an alist");
let dumped_with = stats.cons_car();
assert_eq!(dumped_with.cons_car(), Value::symbol("dumped-with-pdumper"));
assert_eq!(dumped_with.cons_cdr(), Value::T);
let load_time = stats.cons_cdr().cons_car();
assert_eq!(load_time.cons_car(), Value::symbol("load-time"));
assert!(load_time.cons_cdr().is_float());
let dump_file = stats.cons_cdr().cons_cdr().cons_car();
assert_eq!(dump_file.cons_car(), Value::symbol("dump-file-name"));
let expected = dump_path
.canonicalize()
.unwrap()
.to_string_lossy()
.into_owned();
assert_eq!(
dump_file.cons_cdr().as_str_owned().as_deref(),
Some(expected.as_str())
);
}
#[test]
fn test_pdump_bad_magic() {
crate::test_utils::init_test_tracing();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bad.pdump");
std::fs::write(&path, b"BADMAGIC").unwrap();
assert!(matches!(load_from_dump(&path), Err(DumpError::BadMagic)));
}
#[test]
fn test_pdump_round_trip_bootstrap() {
crate::test_utils::init_test_tracing();
let eval = crate::emacs_core::load::create_bootstrap_evaluator()
.expect("bootstrap should succeed");
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("bootstrap.pdump");
let dump_start = std::time::Instant::now();
dump_to_file(&eval, &dump_path).expect("dump should succeed");
let dump_time = dump_start.elapsed();
let file_size = std::fs::metadata(&dump_path).unwrap().len();
eprintln!(
"pdump: dump took {dump_time:.2?}, file size: {file_size} bytes ({:.1} MB)",
file_size as f64 / 1048576.0
);
drop(eval);
let load_start = std::time::Instant::now();
let mut loaded = load_from_dump(&dump_path).expect("load should succeed");
let load_time = load_start.elapsed();
eprintln!("pdump: load took {load_time:.2?}");
let forms = crate::emacs_core::value_reader::read_all("(+ 1 2)").unwrap();
let result = loaded.eval_sub(forms[0]).expect("eval should succeed");
assert_eq!(result, Value::fixnum(3));
let forms = crate::emacs_core::value_reader::read_all("(featurep 'backquote)").unwrap();
let result = loaded
.eval_sub(forms[0])
.expect("featurep should succeed");
assert_eq!(result, Value::T, "featurep 'backquote should be t");
let forms = crate::emacs_core::value_reader::read_all("(length '(a b c))").unwrap();
let result = loaded.eval_sub(forms[0]).expect("eval should succeed");
assert_eq!(result, Value::fixnum(3));
let forms =
crate::emacs_core::value_reader::read_all("(concat \"hello\" \" \" \"world\")").unwrap();
let result = loaded.eval_sub(forms[0]).expect("eval should succeed");
assert_eq!(crate::emacs_core::print_value(&result), "\"hello world\"");
let forms = crate::emacs_core::value_reader::read_all(
"(let ((h (make-hash-table :test 'equal))) (puthash \"key\" 42 h) (gethash \"key\" h))",
)
.unwrap();
let result = loaded.eval_sub(forms[0]).expect("eval should succeed");
assert_eq!(result, Value::fixnum(42));
let forms = crate::emacs_core::value_reader::read_all(
"(progn (defun pdump-test-fn (x) (* x x)) (pdump-test-fn 7))",
)
.unwrap();
let result = loaded.eval_sub(forms[0]).expect("eval should succeed");
assert_eq!(result, Value::fixnum(49));
}
#[test]
fn test_pdump_round_trip_preserves_runtime_derived_mode_syntax() {
crate::test_utils::init_test_tracing();
let mut eval = crate::emacs_core::load::create_bootstrap_evaluator()
.expect("bootstrap should succeed");
crate::emacs_core::load::apply_runtime_startup_state(&mut eval)
.expect("runtime startup should succeed");
let probe_src = r#"(list
(boundp 'lisp-data-mode-syntax-table)
(boundp 'emacs-lisp-mode-syntax-table)
(boundp 'lisp-interaction-mode-syntax-table)
(functionp (symbol-function 'lisp-interaction-mode))
(eq (char-table-parent emacs-lisp-mode-syntax-table)
lisp-data-mode-syntax-table)
(eq (char-table-parent lisp-interaction-mode-syntax-table)
emacs-lisp-mode-syntax-table)
(char-syntax ?\n)
(char-syntax ?\;)
(char-syntax ?{)
(char-syntax ?'))"#;
let probe = crate::emacs_core::value_reader::read_all(probe_src).unwrap();
let full_result = eval
.eval_sub(probe[0])
.expect("full bootstrap probe should run");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&full_result, &eval.buffers),
"(t t t t t t 62 60 95 39)"
);
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("derived-mode-syntax.pdump");
dump_to_file(&eval, &dump_path).expect("dump should succeed");
drop(eval);
let mut loaded = load_from_dump(&dump_path).expect("load should succeed");
crate::emacs_core::load::apply_runtime_startup_state(&mut loaded)
.expect("runtime startup after load should succeed");
let probe = crate::emacs_core::value_reader::read_all(probe_src).unwrap();
let loaded_result = loaded
.eval_sub(probe[0])
.expect("loaded bootstrap probe should run");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&loaded_result, &loaded.buffers),
"(t t t t t t 62 60 95 39)"
);
}
#[test]
fn test_pdump_round_trip_preserves_pre_runtime_standard_syntax_identity() {
crate::test_utils::init_test_tracing();
let eval = crate::emacs_core::load::create_bootstrap_evaluator()
.expect("bootstrap should succeed");
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("bootstrap-pre-runtime-syntax.pdump");
dump_to_file(&eval, &dump_path).expect("dump should succeed");
drop(eval);
let mut loaded = load_from_dump(&dump_path).expect("load should succeed");
crate::emacs_core::load::apply_runtime_startup_state(&mut loaded)
.expect("runtime startup after load should succeed");
let probe = crate::emacs_core::value_reader::read_all(
r#"(list
(eq (char-table-parent emacs-lisp-mode-syntax-table)
lisp-data-mode-syntax-table)
(eq (char-table-parent lisp-interaction-mode-syntax-table)
emacs-lisp-mode-syntax-table)
(char-syntax ?\n)
(char-syntax ?\;)
(char-syntax ?{)
(char-syntax ?'))"#,
)
.unwrap();
let result = loaded
.eval_sub(probe[0])
.expect("loaded pre-runtime probe should run");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&result, &loaded.buffers),
"(t t 62 60 95 39)"
);
}
#[test]
fn test_pdump_round_trip_preserves_default_fontset_han_order() {
crate::test_utils::init_test_tracing();
let mut eval =
crate::emacs_core::load::create_bootstrap_evaluator_with_features(&["neomacs"])
.expect("bootstrap should succeed");
let setup = crate::emacs_core::value_reader::read_all(
r#"(new-fontset
"fontset-default"
'((han
(nil . "GB2312.1980-0")
(nil . "JISX0208*")
(nil . "gb18030"))))"#,
)
.unwrap();
eval.eval_sub(setup[0])
.expect("han-only fontset should install before dump");
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("bootstrap-charsets.pdump");
dump_to_file(&eval, &dump_path).expect("dump should succeed");
drop(eval);
let mut loaded = load_from_dump(&dump_path).expect("load should succeed");
let probe = crate::emacs_core::value_reader::read_all(
r#"(list
(fontset-font t ?好 t)
(fontset-font t (string-to-char "好") t))"#,
)
.unwrap();
let result = loaded
.eval_sub(probe[0])
.expect("pdump fontset probe should run");
let rendered = crate::emacs_core::print_value_with_buffers(&result, &loaded.buffers);
assert!(
rendered.starts_with(
"(((nil . \"gb2312.1980-0\") \
(nil . \"jisx0208*\") \
(nil . \"gb18030\")) \
((nil . \"gb2312.1980-0\") \
(nil . \"jisx0208*\") \
(nil . \"gb18030\")))"
),
"unexpected pdump fontset order: {rendered}"
);
}
#[test]
fn test_restore_snapshot_isolated_between_clones() {
crate::test_utils::init_test_tracing();
let template = crate::emacs_core::load::create_bootstrap_evaluator_cached()
.expect("bootstrap template should succeed");
let snapshot = snapshot_evaluator(&template);
let mut first = restore_snapshot(&snapshot).expect("first clone should succeed");
let setup = crate::emacs_core::value_reader::read_all(
"(progn
(setq compat-pdump-clone-smoke 'first)
compat-pdump-clone-smoke)",
)
.unwrap();
let first_result = first
.eval_sub(setup[0])
.expect("first clone evaluation should succeed");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&first_result, &first.buffers),
"first"
);
let mut second = restore_snapshot(&snapshot).expect("second clone should succeed");
let probe =
crate::emacs_core::value_reader::read_all("(boundp 'compat-pdump-clone-smoke)").unwrap();
let second_result = second
.eval_sub(probe[0])
.expect("second clone evaluation should succeed");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&second_result, &second.buffers),
"nil"
);
}
#[test]
fn test_restore_snapshot_preserves_core_subr_callable_surface() {
crate::test_utils::init_test_tracing();
let template = Context::new();
let snapshot = snapshot_evaluator(&template);
let mut restored = restore_snapshot(&snapshot).expect("restored snapshot should succeed");
let forms = crate::emacs_core::value_reader::read_all(
r#"(list (funcall 'cons 1 2)
(funcall 'list 1 2 3)
(funcall 'intern "compat-pdump-subr-probe")
(funcall 'format "%s-%s" "pdump" "ok"))"#,
)
.expect("parse");
let result = restored
.eval_sub(forms[0])
.expect("restored runtime subrs should be callable");
assert_eq!(
crate::emacs_core::print_value_with_buffers(&result, &restored.buffers),
"((1 . 2) (1 2 3) compat-pdump-subr-probe \"pdump-ok\")"
);
}
#[test]
fn test_restore_snapshot_does_not_report_file_based_pdump_session() {
crate::test_utils::init_test_tracing();
let mut template = Context::new();
let setup = crate::emacs_core::value_reader::read_all(
"(progn
(setq compat-pdump-snapshot-hook-fired nil)
(setq after-pdump-load-hook
(list (lambda () (setq compat-pdump-snapshot-hook-fired t)))))",
)
.unwrap();
template
.eval_sub(setup[0])
.expect("setup hook should evaluate");
let snapshot = snapshot_evaluator(&template);
let mut restored = restore_snapshot(&snapshot).expect("restored snapshot should succeed");
assert_eq!(
restored
.obarray
.symbol_value("compat-pdump-snapshot-hook-fired"),
Some(&Value::NIL)
);
let forms = crate::emacs_core::value_reader::read_all("(pdumper-stats)").unwrap();
let stats = restored
.eval_sub(forms[0])
.expect("pdumper-stats should evaluate");
assert!(stats.is_nil());
}
#[test]
fn test_pdump_checksum_mismatch() {
crate::test_utils::init_test_tracing();
let dir = tempfile::tempdir().unwrap();
let dump_path = dir.path().join("test.pdump");
let eval = Context::new();
dump_to_file(&eval, &dump_path).expect("dump should succeed");
let mut data = std::fs::read(&dump_path).unwrap();
if let Some(last) = data.last_mut() {
*last ^= 0xFF;
}
std::fs::write(&dump_path, &data).unwrap();
let result = load_from_dump(&dump_path);
assert!(result.is_err());
}
#[test]
fn test_restore_snapshot_rejects_legacy_unwind_protect_dump_opcode() {
crate::test_utils::init_test_tracing();
let mut snapshot = snapshot_evaluator(&Context::new());
snapshot
.tagged_heap
.objects
.push(DumpHeapObject::ByteCode(DumpByteCodeFunction {
ops: vec![DumpOp::UnwindProtect(7), DumpOp::Nil, DumpOp::Return],
constants: vec![],
max_stack: 1,
params: DumpLambdaParams {
required: vec![],
optional: vec![],
rest: None,
},
lexical: false,
env: None,
gnu_byte_offset_map: None,
docstring: None,
doc_form: None,
interactive: None,
}));
let result = restore_snapshot(&snapshot);
match result {
Err(DumpError::DeserializationError(message)) => {
assert!(
message.contains(
"legacy neomacs unwind-protect opcode is unsupported in pdump snapshots"
),
"unexpected error: {message}"
);
}
Ok(_) => panic!("expected deserialization error, got successful restore"),
Err(err) => panic!("expected deserialization error, got {err}"),
}
}
}