use super::builtins::collections::builtin_make_hash_table;
use super::error::{EvalError, Flow, map_flow, signal};
use super::intern::{intern, resolve_sym};
use super::keymap::{is_list_keymap, list_keymap_lookup_one};
use super::value::{HashKey, Value, ValueKind, list_to_vec};
use crate::heap_types::LispString;
use sha2::{Digest, Sha256};
use std::cell::Cell;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(unix)]
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
thread_local! {
static BOOTSTRAP_PREFER_LDEFS_BOOT: Cell<bool> = const { Cell::new(false) };
}
fn load_string_text(value: &Value) -> Option<String> {
value.as_runtime_string_owned()
}
fn load_display_string(value: &LispString) -> String {
crate::emacs_core::emacs_char::to_utf8_lossy(value.as_bytes())
}
fn load_name_equal(left: &LispString, right: &LispString) -> bool {
crate::emacs_core::value::equal_value(
&Value::heap_string(left.clone()),
&Value::heap_string(right.clone()),
0,
)
}
#[cfg(not(unix))]
fn load_runtime_string(value: &LispString) -> String {
super::builtins::runtime_string_from_lisp_string(value)
}
fn load_path_lisp_string(path: &Path) -> LispString {
super::fileio::path_to_lisp_file_name(path)
}
fn load_path_buf(value: &LispString) -> PathBuf {
super::fileio::lisp_file_name_to_path_buf(value)
}
fn load_path_value(path: &Path) -> Value {
Value::heap_string(load_path_lisp_string(path))
}
fn load_found_effective(found: &LispString) -> LispString {
found.clone()
}
fn load_hist_file_name(
eval: &super::eval::Context,
requested: &LispString,
found: &LispString,
) -> LispString {
let found_effective = load_found_effective(found);
if !eval
.obarray()
.symbol_value("purify-flag")
.is_some_and(|value| value.is_truthy())
{
return found_effective;
}
let found_path = load_path_buf(&found_effective);
let Some(file_name) = found_path.file_name() else {
return found_effective;
};
let requested_path = load_path_buf(requested);
let directory = requested_path
.parent()
.filter(|dir| !dir.as_os_str().is_empty());
match directory {
Some(dir) => load_path_lisp_string(&dir.join(file_name)),
None => super::fileio::path_to_lisp_file_name(Path::new(file_name)),
}
}
struct BootstrapLdefsBootPreferenceGuard {
previous: bool,
}
impl BootstrapLdefsBootPreferenceGuard {
fn enable() -> Self {
let previous = BOOTSTRAP_PREFER_LDEFS_BOOT.with(|cell| cell.replace(true));
Self { previous }
}
}
impl Drop for BootstrapLdefsBootPreferenceGuard {
fn drop(&mut self) {
BOOTSTRAP_PREFER_LDEFS_BOOT.with(|cell| cell.set(self.previous));
}
}
fn bootstrap_prefers_ldefs_boot() -> bool {
BOOTSTRAP_PREFER_LDEFS_BOOT.with(Cell::get)
}
pub(crate) fn decode_emacs_utf8(bytes: &[u8]) -> String {
fn push_extended_char_or_escape(out: &mut String, code: u32) {
if out.ends_with('?') {
out.push_str(&format!("\\x{:X}", code));
} else {
out.push('\u{FFFD}');
}
}
let mut out = String::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b < 0x80 {
out.push(b as char);
i += 1;
continue;
}
if b >= 0xC2 && b <= 0xDF && i + 1 < bytes.len() && (bytes[i + 1] & 0xC0) == 0x80 {
if let Some(s) = std::str::from_utf8(&bytes[i..i + 2]).ok() {
out.push_str(s);
i += 2;
continue;
}
}
if b >= 0xE0
&& b <= 0xEF
&& i + 2 < bytes.len()
&& (bytes[i + 1] & 0xC0) == 0x80
&& (bytes[i + 2] & 0xC0) == 0x80
{
if let Some(s) = std::str::from_utf8(&bytes[i..i + 3]).ok() {
out.push_str(s);
i += 3;
continue;
}
}
if b >= 0xF0
&& b <= 0xF4
&& i + 3 < bytes.len()
&& (bytes[i + 1] & 0xC0) == 0x80
&& (bytes[i + 2] & 0xC0) == 0x80
&& (bytes[i + 3] & 0xC0) == 0x80
{
if let Some(s) = std::str::from_utf8(&bytes[i..i + 4]).ok() {
out.push_str(s);
i += 4;
continue;
}
}
if b >= 0xF5
&& b <= 0xF7
&& i + 3 < bytes.len()
&& (bytes[i + 1] & 0xC0) == 0x80
&& (bytes[i + 2] & 0xC0) == 0x80
&& (bytes[i + 3] & 0xC0) == 0x80
{
let code = ((b as u32 & 0x07) << 18)
| ((bytes[i + 1] as u32 & 0x3F) << 12)
| ((bytes[i + 2] as u32 & 0x3F) << 6)
| (bytes[i + 3] as u32 & 0x3F);
push_extended_char_or_escape(&mut out, code);
i += 4;
continue;
}
if b >= 0xF8
&& b <= 0xFB
&& i + 4 < bytes.len()
&& (bytes[i + 1] & 0xC0) == 0x80
&& (bytes[i + 2] & 0xC0) == 0x80
&& (bytes[i + 3] & 0xC0) == 0x80
&& (bytes[i + 4] & 0xC0) == 0x80
{
let code = ((b as u32 & 0x03) << 24)
| ((bytes[i + 1] as u32 & 0x3F) << 18)
| ((bytes[i + 2] as u32 & 0x3F) << 12)
| ((bytes[i + 3] as u32 & 0x3F) << 6)
| (bytes[i + 4] as u32 & 0x3F);
push_extended_char_or_escape(&mut out, code);
i += 5;
continue;
}
if b >= 0xFC
&& b <= 0xFD
&& i + 5 < bytes.len()
&& (bytes[i + 1] & 0xC0) == 0x80
&& (bytes[i + 2] & 0xC0) == 0x80
&& (bytes[i + 3] & 0xC0) == 0x80
&& (bytes[i + 4] & 0xC0) == 0x80
&& (bytes[i + 5] & 0xC0) == 0x80
{
let code = ((b as u32 & 0x01) << 30)
| ((bytes[i + 1] as u32 & 0x3F) << 24)
| ((bytes[i + 2] as u32 & 0x3F) << 18)
| ((bytes[i + 3] as u32 & 0x3F) << 12)
| ((bytes[i + 4] as u32 & 0x3F) << 6)
| (bytes[i + 5] as u32 & 0x3F);
push_extended_char_or_escape(&mut out, code);
i += 6;
continue;
}
out.push('\u{FFFD}');
i += 1;
}
out
}
fn format_value_for_error(v: &Value) -> String {
match v.kind() {
ValueKind::Symbol(sid) => super::intern::resolve_sym(sid).to_string(),
ValueKind::String => format!("\"{}\"", load_string_text(v).expect("checked string")),
ValueKind::Fixnum(n) => format!("{}", n),
ValueKind::Nil => "nil".to_string(),
ValueKind::T => "t".to_string(),
ValueKind::Cons => {
let car = v.cons_car();
let cdr = v.cons_cdr();
let car_s = format_value_for_error(&car);
let cdr_s = format_value_for_error(&cdr);
if cdr == Value::NIL {
format!("({})", car_s)
} else {
format!("({} . {})", car_s, cdr_s)
}
}
other => format!("{:?}", v),
}
}
fn format_eval_error_in_state(eval: &super::eval::Context, err: &EvalError) -> String {
match err {
EvalError::Signal {
symbol,
data,
raw_data,
} => {
let payload = if let Some(raw) = raw_data {
crate::emacs_core::error::print_value_in_state(eval, raw)
} else if data.is_empty() {
"nil".to_string()
} else {
crate::emacs_core::error::print_value_in_state(eval, &Value::list(data.clone()))
};
format!("({} {})", resolve_sym(*symbol), payload)
}
EvalError::UncaughtThrow { tag, value } => format!(
"(throw {} {})",
crate::emacs_core::error::print_value_in_state(eval, tag),
crate::emacs_core::error::print_value_in_state(eval, value),
),
}
}
fn is_kill_emacs_signal(err: &EvalError) -> bool {
matches!(
err,
EvalError::Signal { symbol, .. } if resolve_sym(*symbol) == "kill-emacs"
)
}
fn format_load_form_error(err: &EvalError) -> String {
match err {
EvalError::Signal {
symbol,
data,
raw_data,
} => {
let payload = if let Some(raw) = raw_data {
format_value_for_error(raw)
} else if data.is_empty() {
"nil".to_string()
} else {
let data_strs: Vec<String> = data.iter().map(format_value_for_error).collect();
format!("({})", data_strs.join(" "))
};
format!("({} {})", resolve_sym(*symbol), payload)
}
other => format!("{other:?}"),
}
}
fn log_streaming_load_form_error(
eval: &super::eval::Context,
file_name: &str,
form_idx: usize,
preview: String,
err: &EvalError,
) {
tracing::error!(
" !! {} FORM[{}] FAILED: {} => {}",
file_name,
form_idx,
preview,
format_load_form_error(err),
);
let bt_frames: Vec<_> = eval
.specpdl
.iter()
.rev()
.filter_map(|entry| match entry {
super::eval::SpecBinding::Backtrace { function, args, .. } => Some((function, args)),
_ => None,
})
.collect();
if !bt_frames.is_empty() {
tracing::error!(" Lisp backtrace:");
for (j, (function, frame_args)) in bt_frames.iter().enumerate() {
let func_name = super::print::print_value(function);
let args_str = frame_args
.as_slice()
.iter()
.take(4)
.map(|a| {
let s = super::print::print_value(a);
if s.len() > 40 {
format!("{}...", &s[..37])
} else {
s
}
})
.collect::<Vec<_>>()
.join(" ");
let ellipsis = if frame_args.as_slice().len() > 4 {
" ..."
} else {
""
};
tracing::error!(" {j}: ({func_name} {args_str}{ellipsis})");
if j >= 20 {
tracing::error!(" ... ({} more frames)", bt_frames.len() - j - 1);
break;
}
}
}
}
const COMPILED_ELISP_FORM_PREVIEW: &str = "<compiled .elc form elided>";
fn is_compiled_elisp_path(path: &Path) -> bool {
path.extension().is_some_and(|extension| extension == "elc")
}
fn load_form_log_preview(path: &Path, make_preview: impl FnOnce() -> String) -> String {
if is_compiled_elisp_path(path) {
COMPILED_ELISP_FORM_PREVIEW.to_string()
} else {
make_preview()
}
}
const GENERATED_LOADDEFS_MARKER: &str = "Generated by the `loaddefs-generate' function.";
const TRANSIENT_RUNTIME_FEATURES: &[&str] = &[
"cl-lib", "cl-macs", "cl-seq", "cl-extra", "gv", "icons", "pcase",
];
fn is_generated_loaddefs_source(source: &str) -> bool {
source.contains(GENERATED_LOADDEFS_MARKER)
}
fn eval_generated_form_args(
eval: &mut super::eval::Context,
args: &[Value],
) -> Result<Vec<Value>, EvalError> {
args.iter()
.map(|value| eval_runtime_form(eval, *value))
.collect()
}
fn eval_runtime_form(eval: &mut super::eval::Context, form: Value) -> Result<Value, EvalError> {
eval.eval_sub(form).map_err(map_flow)
}
fn cached_form_requires_eager_replay(form: Value) -> bool {
form.is_cons()
&& form
.cons_car()
.as_symbol_name()
.is_some_and(|name| matches!(name, "eval-and-compile" | "eval-when-compile"))
}
fn generated_defalias(eval: &mut super::eval::Context, args: &[Value]) -> Result<Value, EvalError> {
if !(2..=3).contains(&args.len()) {
return Err(EvalError::Signal {
symbol: intern("wrong-number-of-arguments"),
data: vec![Value::symbol("defalias"), Value::fixnum(args.len() as i64)],
raw_data: None,
});
}
let values = eval_generated_form_args(eval, args)?;
let result = eval
.defalias_value(values[0], values[1])
.map_err(map_flow)?;
if let Some(doc) = values.get(2).copied().filter(|value| !value.is_nil()) {
super::builtins::builtin_put(
eval,
vec![values[0], Value::symbol("function-documentation"), doc],
)
.map_err(map_flow)?;
}
Ok(result)
}
fn try_eval_generated_loaddefs_form(
eval: &mut super::eval::Context,
form: Value,
) -> Result<Option<Value>, EvalError> {
let Some(items) = list_to_vec(&form) else {
return Ok(None);
};
let Some(head_sym) = items.first().and_then(|v| v.as_symbol_name()) else {
return Ok(None);
};
let tail = &items[1..];
match head_sym {
"progn" => {
let mut last = Value::NIL;
for item in tail {
last = eval_generated_loaddefs_form(eval, *item)?;
}
Ok(Some(last))
}
"autoload" => {
let values = eval_generated_form_args(eval, tail)?;
Ok(Some(
super::autoload::builtin_autoload(eval, values).map_err(map_flow)?,
))
}
"put" | "function-put" => {
let values = eval_generated_form_args(eval, tail)?;
Ok(Some(
super::builtins::builtin_put(eval, values).map_err(map_flow)?,
))
}
"defalias" => Ok(Some(generated_defalias(eval, tail)?)),
"defvaralias" => {
let values = eval_generated_form_args(eval, tail)?;
Ok(Some(
super::builtins::builtin_defvaralias(eval, values).map_err(map_flow)?,
))
}
_ => Ok(None),
}
}
fn eval_generated_loaddefs_form(
eval: &mut super::eval::Context,
form: Value,
) -> Result<Value, EvalError> {
if let Some(value) = try_eval_generated_loaddefs_form(eval, form)? {
return Ok(value);
}
eval_runtime_form(eval, form)
}
fn has_load_suffix(name: &LispString) -> bool {
let bytes = name.as_bytes();
bytes.ends_with(b".el") || bytes.ends_with(b".elc")
}
fn append_load_suffix(base: &Path, suffix: &[u8]) -> PathBuf {
#[cfg(unix)]
{
let mut bytes = base.as_os_str().as_bytes().to_vec();
bytes.extend_from_slice(suffix);
PathBuf::from(std::ffi::OsString::from_vec(bytes))
}
#[cfg(not(unix))]
{
let suffix = std::str::from_utf8(suffix).expect("ASCII suffix");
PathBuf::from(format!("{}{}", base.to_string_lossy(), suffix))
}
}
fn source_suffixed_path(base: &Path) -> PathBuf {
append_load_suffix(base, b".el")
}
fn compiled_suffixed_path(base: &Path) -> PathBuf {
append_load_suffix(base, b".elc")
}
fn unsupported_compiled_suffixed_paths(base: &Path) -> [PathBuf; 1] {
[append_load_suffix(base, b".elc.gz")]
}
fn prefer_el_only() -> bool {
std::env::var("NEOVM_PREFER_EL").is_ok()
}
fn candidate_mtime(path: &Path) -> Option<std::time::SystemTime> {
fs::metadata(path).ok()?.modified().ok()
}
fn pick_suffixed(base: &Path, prefer_newer: bool) -> Option<PathBuf> {
let el = source_suffixed_path(base);
let elc = compiled_suffixed_path(base);
let skip_elc = prefer_el_only();
if prefer_newer && !skip_elc {
let mut candidates = Vec::new();
if elc.exists() {
candidates.push(elc.clone());
}
if el.exists() {
candidates.push(el.clone());
}
return candidates
.into_iter()
.filter_map(|path| candidate_mtime(&path).map(|mtime| (mtime, path)))
.max_by_key(|(mtime, _)| *mtime)
.map(|(_, path)| path);
}
if !skip_elc && elc.exists() {
return Some(elc);
}
if el.exists() {
return Some(el);
}
None
}
fn find_for_base(
base: &Path,
original_name: &LispString,
no_suffix: bool,
must_suffix: bool,
prefer_newer: bool,
) -> Option<PathBuf> {
if no_suffix || has_load_suffix(original_name) {
if base.is_file() {
return Some(base.to_path_buf());
}
return None;
}
if let Some(suffixed) = pick_suffixed(base, prefer_newer) {
return Some(suffixed);
}
if !must_suffix && base.is_file() {
return Some(base.to_path_buf());
}
for compiled in unsupported_compiled_suffixed_paths(base) {
if compiled.exists() {
return Some(compiled);
}
}
None
}
fn expand_tilde_path_buf(path: &LispString) -> PathBuf {
#[cfg(unix)]
{
let bytes = path.as_bytes();
if bytes == b"~" {
if let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home);
}
} else if bytes.starts_with(b"~/") {
if let Some(home) = std::env::var_os("HOME") {
let mut expanded = PathBuf::from(home);
expanded.push(std::ffi::OsString::from_vec(bytes[2..].to_vec()));
return expanded;
}
}
return load_path_buf(path);
}
#[cfg(not(unix))]
{
PathBuf::from(expand_tilde(&load_runtime_string(path)))
}
}
#[tracing::instrument(level = "debug", ret)]
pub fn find_file_in_load_path(name: &str, load_path: &[LispString]) -> Option<PathBuf> {
find_file_in_load_path_with_flags(name, load_path, false, false, false)
}
pub fn find_file_in_load_path_with_flags(
name: &str,
load_path: &[LispString],
no_suffix: bool,
must_suffix: bool,
prefer_newer: bool,
) -> Option<PathBuf> {
let name = LispString::from_utf8(name);
find_lisp_file_in_load_path_with_flags(&name, load_path, no_suffix, must_suffix, prefer_newer)
.map(|found| load_path_buf(&found))
}
fn find_lisp_file_in_load_path_with_flags(
name: &LispString,
load_path: &[LispString],
no_suffix: bool,
must_suffix: bool,
prefer_newer: bool,
) -> Option<LispString> {
let path = expand_tilde_path_buf(name);
if path.is_absolute() {
return find_for_base(&path, name, no_suffix, must_suffix, prefer_newer)
.map(|found| load_path_lisp_string(&found));
}
if bootstrap_prefers_ldefs_boot()
&& !no_suffix
&& !must_suffix
&& matches!(name.as_bytes(), b"loaddefs" | b"loaddefs.el")
{
for dir in load_path {
let bootstrap = load_path_buf(dir).join("ldefs-boot.el");
if bootstrap.is_file() {
return Some(load_path_lisp_string(&bootstrap));
}
}
}
for dir in load_path {
let full = load_path_buf(dir).join(load_path_buf(name));
if let Some(found) = find_for_base(&full, name, no_suffix, must_suffix, prefer_newer) {
return Some(load_path_lisp_string(&found));
}
}
None
}
pub fn get_load_path(obarray: &super::symbol::Obarray) -> Vec<LispString> {
let default_directory = obarray
.symbol_value("default-directory")
.and_then(|v| {
v.is_string()
.then(|| v.as_lisp_string().expect("checked string").clone())
})
.unwrap_or_else(|| LispString::from_unibyte(b".".to_vec()));
let val = obarray
.symbol_value("load-path")
.cloned()
.unwrap_or(Value::NIL);
super::value::list_to_vec(&val)
.unwrap_or_default()
.into_iter()
.filter_map(|v| match v {
v if v.is_nil() => Some(default_directory.clone()),
_ if v.is_string() => v.as_lisp_string().cloned(),
_ => None,
})
.collect()
}
pub(crate) enum LoadPlan {
Return(Value),
Load {
requested: LispString,
found: LispString,
},
}
pub(crate) fn plan_load_in_state(
obarray: &super::symbol::Obarray,
file: Value,
noerror: Option<Value>,
nosuffix: Option<Value>,
must_suffix: Option<Value>,
) -> Result<LoadPlan, Flow> {
let file = match file.kind() {
ValueKind::String => file.as_lisp_string().expect("checked string").clone(),
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), file],
));
}
};
let file = super::fileio::substitute_in_file_name_lisp(&file);
let noerror = noerror.is_some_and(|v| v.is_truthy());
let nosuffix = nosuffix.is_some_and(|v| v.is_truthy());
let must_suffix = must_suffix.is_some_and(|v| v.is_truthy());
let prefer_newer = obarray
.symbol_value("load-prefer-newer")
.is_some_and(|v| v.is_truthy());
let load_path = get_load_path(obarray);
match find_lisp_file_in_load_path_with_flags(
&file,
&load_path,
nosuffix,
must_suffix,
prefer_newer,
) {
Some(found) => Ok(LoadPlan::Load {
requested: file,
found,
}),
None => {
if noerror {
Ok(LoadPlan::Return(Value::NIL))
} else {
Err(signal(
"file-missing",
vec![Value::string(format!(
"Cannot open load file: {}",
load_display_string(&file)
))],
))
}
}
}
}
pub(crate) fn builtin_load_in_vm_runtime(
shared: &mut super::eval::Context,
args: &[Value],
) -> Result<Value, Flow> {
if args.is_empty() {
return Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol("load"), Value::fixnum(0)],
));
}
match plan_load_in_state(
&shared.obarray,
args[0],
args.get(1).copied(),
args.get(3).copied(),
args.get(4).copied(),
)? {
LoadPlan::Return(value) => Ok(value),
LoadPlan::Load { requested, found } => {
let extra_roots = args.to_vec();
let noerror = args.get(1).is_some_and(|v| v.is_truthy());
let nomessage = args.get(2).is_some_and(|v| v.is_truthy());
let path = load_path_buf(&found);
let root_scope = shared.save_specpdl_roots();
for root in &extra_roots {
shared.push_specpdl_root(*root);
}
let result = load_file_with_requested_and_found_flags(
shared, &path, &requested, &found, noerror, nomessage,
)
.map_err(|e| match e {
EvalError::Signal {
symbol,
data,
raw_data,
} => Flow::Signal(Box::new(crate::emacs_core::error::SignalData {
symbol,
data,
raw_data,
suppress_signal_hook: false,
selected_resume: None,
search_complete: false,
})),
EvalError::UncaughtThrow { tag, value } => Flow::Throw { tag, value },
});
shared.restore_specpdl_roots(root_scope);
result
}
}
}
pub(crate) const BOOTSTRAP_LOAD_PATH_SUBDIRS: &[&str] = &[
"",
"calendar",
"emacs-lisp",
"mail",
"progmodes",
"language",
"international",
"textmodes",
"vc",
"leim",
];
fn strip_utf8_bom(source: &str) -> &str {
source.strip_prefix('\u{feff}').unwrap_or(source)
}
fn strip_utf8_bom_bytes(source: &[u8]) -> &[u8] {
source.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(source)
}
fn strip_reader_prefix(source: &str) -> (&str, bool) {
let without_bom = strip_utf8_bom(source);
if !without_bom.starts_with("#!") {
return (without_bom, false);
}
match without_bom.find('\n') {
Some(index) => (&without_bom[index + 1..], false),
None => ("", true),
}
}
fn lexical_binding_enabled_in_file_local_cookie_line(line: &str) -> bool {
matches!(
lexical_binding_cookie_in_file_local_cookie_line(line),
LexicalBindingCookie::Lexical
)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum LexicalBindingCookie {
None,
Dynamic,
Lexical,
}
fn lexical_binding_cookie_in_file_local_cookie_line(line: &str) -> LexicalBindingCookie {
let Some(start) = line.find("-*-") else {
return LexicalBindingCookie::None;
};
let rest = &line[start + 3..];
let Some(end_rel) = rest.find("-*-") else {
return LexicalBindingCookie::None;
};
let cookie = &rest[..end_rel];
for entry in cookie.split(';') {
let Some((name, value)) = entry.split_once(':') else {
continue;
};
if name.trim() == "lexical-binding" {
return if value.trim() == "t" {
LexicalBindingCookie::Lexical
} else {
LexicalBindingCookie::Dynamic
};
}
}
LexicalBindingCookie::None
}
fn trim_cookie_ascii(bytes: &[u8]) -> &[u8] {
let start = bytes
.iter()
.position(|byte| *byte != b' ' && *byte != b'\t')
.unwrap_or(bytes.len());
let end = bytes
.iter()
.rposition(|byte| *byte != b' ' && *byte != b'\t')
.map(|index| index + 1)
.unwrap_or(start);
&bytes[start..end]
}
fn lexical_binding_cookie_in_file_local_cookie_line_bytes(line: &[u8]) -> LexicalBindingCookie {
let Some(start) = line.windows(3).position(|window| window == b"-*-") else {
return LexicalBindingCookie::None;
};
let rest = &line[start + 3..];
let Some(end_rel) = rest.windows(3).position(|window| window == b"-*-") else {
return LexicalBindingCookie::None;
};
let cookie = &rest[..end_rel];
for entry in cookie.split(|byte| *byte == b';') {
let Some(colon) = entry.iter().position(|byte| *byte == b':') else {
continue;
};
let name = trim_cookie_ascii(&entry[..colon]);
let value = trim_cookie_ascii(&entry[colon + 1..]);
if name == b"lexical-binding" {
return if value == b"t" {
LexicalBindingCookie::Lexical
} else {
LexicalBindingCookie::Dynamic
};
}
}
LexicalBindingCookie::None
}
pub(crate) fn lexical_binding_cookie_for_source(source: &str) -> LexicalBindingCookie {
let mut lines = strip_utf8_bom(source).lines();
let first_line = lines.next();
if let Some(cookie) = first_line.map(lexical_binding_cookie_in_file_local_cookie_line)
&& cookie != LexicalBindingCookie::None
{
return cookie;
}
if first_line.is_some_and(|line| line.starts_with("#!")) {
return lines
.next()
.map(lexical_binding_cookie_in_file_local_cookie_line)
.unwrap_or(LexicalBindingCookie::None);
}
LexicalBindingCookie::None
}
pub(crate) fn lexical_binding_cookie_for_lisp_source(source: &LispString) -> LexicalBindingCookie {
let mut lines = strip_utf8_bom_bytes(source.as_bytes()).split(|byte| *byte == b'\n');
let Some(first_line) = lines.next() else {
return LexicalBindingCookie::None;
};
if first_line.starts_with(b"#!") {
return lines
.next()
.map(lexical_binding_cookie_in_file_local_cookie_line_bytes)
.unwrap_or(LexicalBindingCookie::None);
}
lexical_binding_cookie_in_file_local_cookie_line_bytes(first_line)
}
pub(crate) fn lexical_binding_enabled_for_source(source: &str) -> bool {
matches!(
lexical_binding_cookie_for_source(source),
LexicalBindingCookie::Lexical
)
}
fn default_toplevel_lexical_binding(eval: &super::eval::Context) -> bool {
crate::emacs_core::eval::default_toplevel_value_in_state(
&eval.obarray,
eval.specpdl.as_slice(),
Some(&eval.buffers.buffer_defaults),
intern("lexical-binding"),
)
.is_some_and(|value| value.is_truthy())
}
fn lexical_binding_from_cookie(
eval: &mut super::eval::Context,
cookie: LexicalBindingCookie,
from: Option<Value>,
) -> Result<bool, EvalError> {
match cookie {
LexicalBindingCookie::Lexical => Ok(true),
LexicalBindingCookie::Dynamic => Ok(false),
LexicalBindingCookie::None => {
let default = default_toplevel_lexical_binding(eval);
let Some(from) = from else {
return Ok(default);
};
let hook = eval
.visible_variable_value_or_nil("internal--get-default-lexical-binding-function");
if hook.is_nil() {
return Ok(default);
}
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(hook);
eval.push_specpdl_root(from);
let result = eval.apply1(hook, from).map_err(map_flow);
eval.restore_specpdl_roots(roots);
result.map(|value| value.is_truthy())
}
}
}
pub(crate) fn source_lexical_binding_for_load(
eval: &mut super::eval::Context,
source: &str,
from: Option<Value>,
) -> Result<bool, EvalError> {
lexical_binding_from_cookie(eval, lexical_binding_cookie_for_source(source), from)
}
pub(crate) fn source_lexical_binding_for_lisp_source(
eval: &mut super::eval::Context,
source: &LispString,
from: Option<Value>,
) -> Result<bool, EvalError> {
lexical_binding_from_cookie(eval, lexical_binding_cookie_for_lisp_source(source), from)
}
fn is_unsupported_compiled_path(path: &Path) -> bool {
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
return false;
};
name.ends_with(".elc.gz")
}
#[tracing::instrument(level = "debug", skip(eval))]
pub(crate) fn get_eager_macroexpand_fn(eval: &super::eval::Context) -> Option<Value> {
if let Some(val) = eval.obarray().symbol_value("macroexp--pending-eager-loads") {
if val.is_cons() {
if val.cons_car().is_symbol_named("skip") {
return None;
}
}
}
eval.obarray().symbol_function("`--pcase-macroexpander")?;
let f = eval
.obarray()
.symbol_function("internal-macroexpand-for-load")?;
if f.is_nil() {
return None;
}
Some(Value::symbol("internal-macroexpand-for-load"))
}
#[tracing::instrument(level = "debug", skip(eval, form_value, macroexpand_fn, sink))]
pub(crate) fn eager_expand_toplevel_forms(
eval: &mut super::eval::Context,
form_value: Value,
macroexpand_fn: Value,
sink: &mut impl FnMut(&mut super::eval::Context, Value, Value, bool) -> Result<Value, EvalError>,
) -> Result<Value, EvalError> {
let original_form = form_value;
let mutation_epoch_before = eval.macro_expansion_mutation_epoch();
let step1_start = std::time::Instant::now();
let step1_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form_value);
eval.push_specpdl_root(macroexpand_fn);
let val = eval.apply2(macroexpand_fn, form_value, Value::NIL).ok();
eval.restore_specpdl_roots(step1_roots);
eval.note_eager_macro_perf_step1(step1_start.elapsed());
let val = match val {
Some(v) => v,
None => {
tracing::debug!("eager_expand step1 failed, falling back to plain eval");
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form_value);
let result = sink(eval, original_form, form_value, false);
eval.restore_specpdl_roots(roots);
return result;
}
};
if val.is_cons() {
let car = val.cons_car();
let cdr = val.cons_cdr();
if car.is_symbol_named("progn") {
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(val);
let result = (|| -> Result<Value, EvalError> {
let mut result = Value::NIL;
let mut tail = cdr;
while tail.is_cons() {
let sub_form = tail.cons_car();
tail = tail.cons_cdr();
result = eager_expand_toplevel_forms(eval, sub_form, macroexpand_fn, sink)?;
}
Ok(result)
})();
eval.restore_specpdl_roots(roots);
return result;
}
}
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(val);
eval.push_specpdl_root(macroexpand_fn);
eval.push_specpdl_root(original_form);
let t3 = std::time::Instant::now();
let expanded = match eval.apply2(macroexpand_fn, val, Value::T) {
Ok(v) => v,
Err(e) => {
let form_str = super::print::print_value(&val);
let form_preview: String = form_str.chars().take(200).collect();
tracing::debug!("eager_expand step3 failed: {e:?} form={form_preview}");
val
}
};
let d3 = t3.elapsed();
eval.note_eager_macro_perf_step3(d3);
if eval.macro_perf_enabled() && d3.as_millis() > 200 {
let head = if val.is_cons() {
val.cons_car().as_symbol_name().unwrap_or("<non-symbol>")
} else {
"<atom>"
};
let form_str = super::print::print_value(&val);
let form_preview: String = form_str.chars().take(200).collect();
tracing::warn!(
"eager_expand step3 (full-expand) took {d3:.2?} head={head} form={form_preview}"
);
}
let requires_eager_replay = eval.macro_expansion_mutation_epoch() != mutation_epoch_before
|| cached_form_requires_eager_replay(original_form)
|| cached_form_requires_eager_replay(val)
|| cached_form_requires_eager_replay(expanded);
eval.push_specpdl_root(expanded);
let result = sink(eval, original_form, expanded, requires_eager_replay);
eval.restore_specpdl_roots(roots);
result
}
#[tracing::instrument(level = "debug", skip(eval, form_value, macroexpand_fn))]
pub(crate) fn eager_expand_eval(
eval: &mut super::eval::Context,
form_value: Value,
macroexpand_fn: Value,
) -> Result<Value, EvalError> {
eager_expand_toplevel_forms(
eval,
form_value,
macroexpand_fn,
&mut |ctx, _original, expanded, _requires_eager_replay| {
let roots = ctx.save_specpdl_roots();
ctx.push_specpdl_root(expanded);
let t4 = std::time::Instant::now();
let value = ctx.eval_value(&expanded).map_err(map_flow);
let d4 = t4.elapsed();
ctx.note_eager_macro_perf_step4(d4);
if ctx.macro_perf_enabled() && d4.as_millis() > 200 {
tracing::warn!("eager_expand step4 (eval) took {d4:.2?}");
}
ctx.restore_specpdl_roots(roots);
value
},
)
}
fn with_load_context<F>(
eval: &mut super::eval::Context,
hist_file_name: &LispString,
found: &LispString,
lexical_binding: bool,
body: F,
) -> Result<Value, EvalError>
where
F: FnOnce(&mut super::eval::Context) -> Result<Value, EvalError>,
{
let old_reader_load_file = super::value_reader::get_reader_load_file_name_public();
let old_macro_cache_disabled = eval.macro_cache_disabled;
let specpdl_count = eval.specpdl.len();
eval.specbind(intern("lexical-binding"), Value::NIL);
eval.set_runtime_binding_by_id(intern("lexical-binding"), Value::bool_val(lexical_binding));
{
use super::eval::SpecBinding;
eval.specpdl.push(SpecBinding::LexicalEnv {
old_lexenv: eval.lexenv,
});
}
if lexical_binding {
eval.lexenv = Value::list(vec![Value::T]);
} else {
eval.lexenv = Value::NIL;
}
let roots = eval.save_specpdl_roots();
if let Some(v) = old_reader_load_file {
eval.push_specpdl_root(v);
}
let load_file_value = Value::heap_string(hist_file_name.clone());
eval.push_specpdl_root(load_file_value);
let load_true_file_value = Value::heap_string(found.clone());
eval.push_specpdl_root(load_true_file_value);
let current_load_list = Value::cons(load_file_value, Value::NIL);
eval.push_specpdl_root(current_load_list);
eval.specbind(intern("load-file-name"), load_file_value);
eval.specbind(intern("load-true-file-name"), load_true_file_value);
eval.specbind(intern("current-load-list"), current_load_list);
super::value_reader::set_reader_load_file_name(Some(load_file_value));
eval.macro_cache_disabled = true;
let result = body(eval);
super::value_reader::set_reader_load_file_name(old_reader_load_file);
eval.macro_cache_disabled = old_macro_cache_disabled;
eval.unbind_to(specpdl_count);
eval.restore_specpdl_roots(roots);
result
}
fn streaming_readevalloop(
eval: &mut super::eval::Context,
path: &Path,
hist_file_name: &LispString,
content: &str,
source_multibyte: bool,
macroexpand_fn: Option<Value>,
) -> Result<Value, EvalError> {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let load_specpdl_base = eval.specpdl.len();
let mut pos = 0;
let mut form_idx = 0;
loop {
debug_assert_eq!(
eval.specpdl.len(),
load_specpdl_base,
"streaming_readevalloop leaked specpdl entries before {file_name} form {form_idx}",
);
let read_result =
super::value_reader::read_one_with_source_multibyte(content, source_multibyte, pos)
.map_err(|e| {
if e.message.contains("end of input") || e.message.contains("unterminated") {
return EvalError::Signal {
symbol: intern("end-of-file"),
data: vec![],
raw_data: None,
};
}
EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"Read error in {}: {} at position {}",
path.display(),
e.message,
e.position
))],
raw_data: None,
}
})?;
let Some((form, next_pos)) = read_result else {
break; };
let form_start = pos;
pos = next_pos;
if tracing::enabled!(tracing::Level::DEBUG) {
let preview = load_form_log_preview(path, || {
content[form_start..next_pos].chars().take(160).collect()
});
tracing::debug!(
"{} FORM[{}/streaming]: {}",
file_name,
form_idx,
preview.replace('\n', " ")
);
}
let eval_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form);
let eval_result = if let Some(mexp) = macroexpand_fn {
eval.push_specpdl_root(mexp);
streaming_readevalloop_eager_expand_eval(eval, form, mexp)
} else {
eval.eval_sub(form).map_err(map_flow)
};
eval.restore_specpdl_roots(eval_roots);
if let Err(ref e) = eval_result
&& !is_kill_emacs_signal(e)
{
let preview = load_form_log_preview(path, || {
content[form_start..next_pos].chars().take(120).collect()
});
log_streaming_load_form_error(eval, &file_name, form_idx, preview, e);
}
eval_result?;
let gc_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form);
eval.gc_safe_point_exact();
eval.restore_specpdl_roots(gc_roots);
debug_assert_eq!(
eval.specpdl.len(),
load_specpdl_base,
"streaming_readevalloop leaked specpdl entries after {file_name} form {form_idx}",
);
form_idx += 1;
}
build_load_history(eval, hist_file_name, true);
Ok(Value::T)
}
fn streaming_readevalloop_lisp_source(
eval: &mut super::eval::Context,
path: &Path,
hist_file_name: &LispString,
content: &LispString,
macroexpand_fn: Option<Value>,
) -> Result<Value, EvalError> {
let file_name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let read_source = super::value_reader::LispReadSource::new(content);
let load_specpdl_base = eval.specpdl.len();
let mut pos = 0;
let mut form_idx = 0;
loop {
debug_assert_eq!(
eval.specpdl.len(),
load_specpdl_base,
"streaming_readevalloop_lisp_source leaked specpdl entries before {file_name} form {form_idx}",
);
let read_result = read_source.read_one(pos).map_err(|e| {
if e.message.contains("end of input") || e.message.contains("unterminated") {
return EvalError::Signal {
symbol: intern("end-of-file"),
data: vec![],
raw_data: None,
};
}
EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"Read error in {}: {} at position {}",
path.display(),
e.message,
e.position
))],
raw_data: None,
}
})?;
let Some((form, next_pos)) = read_result else {
break;
};
let form_start = pos;
pos = next_pos;
if tracing::enabled!(tracing::Level::DEBUG) {
let preview = load_form_log_preview(path, || {
read_source
.storage_slice_range(form_start, next_pos)
.chars()
.take(160)
.collect()
});
tracing::debug!(
"{} FORM[{}]: {}",
file_name,
form_idx,
preview.replace('\n', " ")
);
}
let eval_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form);
let eval_result = if let Some(mexp) = macroexpand_fn {
eval.push_specpdl_root(mexp);
streaming_readevalloop_eager_expand_eval(eval, form, mexp)
} else {
eval.eval_sub(form).map_err(map_flow)
};
eval.restore_specpdl_roots(eval_roots);
if let Err(ref e) = eval_result
&& !is_kill_emacs_signal(e)
{
let preview = load_form_log_preview(path, || {
read_source
.storage_slice_range(form_start, next_pos)
.chars()
.take(120)
.collect()
});
log_streaming_load_form_error(eval, &file_name, form_idx, preview, e);
}
eval_result?;
let gc_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form);
eval.gc_safe_point_exact();
eval.restore_specpdl_roots(gc_roots);
debug_assert_eq!(
eval.specpdl.len(),
load_specpdl_base,
"streaming_readevalloop_lisp_source leaked specpdl entries after {file_name} form {form_idx}",
);
form_idx += 1;
}
build_load_history(eval, hist_file_name, true);
Ok(Value::T)
}
fn streaming_readevalloop_eager_expand_eval(
eval: &mut super::eval::Context,
form: Value,
macroexpand: Value,
) -> Result<Value, EvalError> {
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(form);
eval.push_specpdl_root(macroexpand);
let step1_start = std::time::Instant::now();
let expanded = match eval.apply2(macroexpand, form, Value::NIL) {
Ok(v) => v,
Err(_) => {
eval.note_eager_macro_perf_step1(step1_start.elapsed());
tracing::debug!("streaming eager_expand step1 failed, falling back to plain eval");
let result = eval.eval_sub(form).map_err(map_flow);
eval.restore_specpdl_roots(roots);
return result;
}
};
eval.note_eager_macro_perf_step1(step1_start.elapsed());
let expanded_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(expanded);
let result = streaming_readevalloop_eager_expand_eval_inner(eval, expanded, macroexpand);
eval.restore_specpdl_roots(expanded_roots);
eval.restore_specpdl_roots(roots);
result
}
fn streaming_readevalloop_eager_expand_eval_inner(
eval: &mut super::eval::Context,
expanded: Value,
macroexpand: Value,
) -> Result<Value, EvalError> {
if expanded.is_cons() && expanded.cons_car().is_symbol_named("progn") {
let mut cursor = expanded.cons_cdr();
let mut last_val = Value::NIL;
while cursor.is_cons() {
let subform = cursor.cons_car();
cursor = cursor.cons_cdr();
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(cursor);
let result = streaming_readevalloop_eager_expand_eval(eval, subform, macroexpand);
eval.restore_specpdl_roots(roots);
last_val = result?;
}
return Ok(last_val);
}
let step3_start = std::time::Instant::now();
let fully_expanded = match eval.apply2(macroexpand, expanded, Value::T) {
Ok(v) => v,
Err(_) => {
tracing::debug!("streaming eager_expand step3 failed, using one-level expansion");
expanded
}
};
eval.note_eager_macro_perf_step3(step3_start.elapsed());
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(fully_expanded);
let step4_start = std::time::Instant::now();
let result = eval.eval_sub(fully_expanded).map_err(map_flow);
eval.note_eager_macro_perf_step4(step4_start.elapsed());
eval.restore_specpdl_roots(roots);
result
}
pub fn load_file(eval: &mut super::eval::Context, path: &Path) -> Result<Value, EvalError> {
load_file_with_flags(eval, path, false, false)
}
pub fn load_file_with_flags(
eval: &mut super::eval::Context,
path: &Path,
noerror: bool,
nomessage: bool,
) -> Result<Value, EvalError> {
let expanded = expand_tilde(&path.to_string_lossy());
let path = std::path::Path::new(&expanded);
tracing::info!("load {}", path.display());
let requested = load_path_lisp_string(path);
load_file_with_requested_and_found_flags(eval, path, &requested, &requested, noerror, nomessage)
}
pub(crate) fn load_file_with_found_flags(
eval: &mut super::eval::Context,
path: &Path,
found: &LispString,
noerror: bool,
nomessage: bool,
) -> Result<Value, EvalError> {
load_file_with_requested_and_found_flags(eval, path, found, found, noerror, nomessage)
}
pub(crate) fn load_file_with_requested_and_found_flags(
eval: &mut super::eval::Context,
path: &Path,
requested: &LispString,
found: &LispString,
noerror: bool,
nomessage: bool,
) -> Result<Value, EvalError> {
if is_unsupported_compiled_path(path) {
return Err(EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"Loading compressed compiled Elisp artifacts (.elc.gz) is unsupported in neomacs: {}",
path.display()
))],
raw_data: None,
});
}
let load_count = eval
.loads_in_progress
.iter()
.filter(|p| load_name_equal(p, found))
.count();
if load_count > 3 {
let found_value = Value::heap_string(found.clone());
let in_progress = Value::list(
eval.loads_in_progress
.iter()
.cloned()
.map(Value::heap_string)
.collect(),
);
return Err(EvalError::Signal {
symbol: intern("error"),
data: vec![
Value::string("Recursive load"),
Value::cons(found_value, in_progress),
],
raw_data: None,
});
}
eval.loads_in_progress.push(found.clone());
let old_load_in_progress = eval
.obarray()
.symbol_value("load-in-progress")
.cloned()
.unwrap_or(Value::NIL);
eval.set_variable("load-in-progress", Value::T);
let result = stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
load_file_body(eval, path, requested, found, noerror, nomessage)
});
eval.set_variable("load-in-progress", old_load_in_progress);
eval.loads_in_progress.pop();
result
}
fn load_file_body(
eval: &mut super::eval::Context,
path: &Path,
requested: &LispString,
found: &LispString,
noerror: bool,
nomessage: bool,
) -> Result<Value, EvalError> {
let is_elc = path.extension().and_then(|e| e.to_str()) == Some("elc");
let hist_file_name = load_hist_file_name(eval, requested, found);
if !is_elc
&& let load_source_file_function =
eval.visible_variable_value_or_nil("load-source-file-function")
&& !load_source_file_function.is_nil()
{
let full_name = Value::heap_string(found.clone());
return eval
.apply(
load_source_file_function,
vec![
full_name,
Value::heap_string(hist_file_name.clone()),
Value::bool_val(noerror),
Value::bool_val(nomessage),
],
)
.map_err(crate::emacs_core::error::map_flow);
}
let raw_bytes = std::fs::read(path).map_err(|e| EvalError::Signal {
symbol: intern("file-error"),
data: vec![Value::string(format!(
"Cannot read file: {}: {}",
path.display(),
e
))],
raw_data: None,
})?;
let (content, source_multibyte) = if is_elc {
(skip_elc_header(&raw_bytes), false)
} else {
let decoded = decode_emacs_utf8(&raw_bytes);
(
match decoded.strip_prefix('\u{feff}') {
Some(rest) => rest.to_string(),
None => decoded,
},
true,
)
};
let lexical_binding = if is_elc {
elc_has_lexical_binding(&raw_bytes)
} else {
source_lexical_binding_for_load(eval, &content, Some(Value::heap_string(found.clone())))?
};
let result = with_load_context(eval, &hist_file_name, found, lexical_binding, |eval| {
let macroexpand_fn = if is_elc {
None
} else {
get_eager_macroexpand_fn(eval)
};
streaming_readevalloop(
eval,
path,
&hist_file_name,
&content,
source_multibyte,
macroexpand_fn,
)
});
if result.is_ok() {
run_after_load_evaluation(eval, &hist_file_name);
}
result
}
pub(crate) fn eval_decoded_source_file_in_context(
eval: &mut super::eval::Context,
path: &Path,
content: &str,
source_multibyte: bool,
) -> Result<Value, EvalError> {
let macroexpand_fn = get_eager_macroexpand_fn(eval);
let found = load_path_lisp_string(path);
streaming_readevalloop(
eval,
path,
&found,
content,
source_multibyte,
macroexpand_fn,
)
}
pub(crate) fn eval_lisp_source_file_in_context(
eval: &mut super::eval::Context,
found: &LispString,
content: &LispString,
) -> Result<Value, EvalError> {
let macroexpand_fn = get_eager_macroexpand_fn(eval);
let path = load_path_buf(found);
streaming_readevalloop_lisp_source(eval, &path, found, content, macroexpand_fn)
}
fn skip_elc_header(raw_bytes: &[u8]) -> String {
let content: String = raw_bytes.iter().map(|&b| b as char).collect();
let mut start = 0;
let bytes = content.as_bytes();
if bytes.starts_with(b";ELC") && bytes.len() >= 8 {
start = 8;
while start < bytes.len() && bytes[start] != b'\n' && bytes[start] != b';' {
start += 1;
}
}
while start < bytes.len() {
if bytes[start] == b'\n' {
start += 1;
continue;
}
if bytes[start] == b';' {
while start < bytes.len() && bytes[start] != b'\n' {
start += 1;
}
continue;
}
break;
}
content[start..].to_string()
}
fn elc_has_lexical_binding(raw_bytes: &[u8]) -> bool {
let preview = std::str::from_utf8(&raw_bytes[..raw_bytes.len().min(1024)]).unwrap_or("");
preview.contains("lexical-binding: t")
}
fn build_load_history(eval: &mut super::eval::Context, filename: &LispString, entire: bool) {
let path_str = load_display_string(filename);
tracing::debug!("build_load_history: {}", path_str);
let roots = eval.save_specpdl_roots();
let current_load_list = eval.visible_variable_value_or_nil("current-load-list");
eval.push_specpdl_root(current_load_list);
let history = eval
.obarray()
.symbol_value("load-history")
.cloned()
.unwrap_or(Value::NIL);
eval.push_specpdl_root(history);
let filtered_history = if entire {
filter_load_history_without_filename(eval, history, filename)
} else {
history
};
eval.push_specpdl_root(filtered_history);
let entry = reverse_copy_rooted_list(eval, current_load_list);
eval.push_specpdl_root(entry);
let updated_history = if entire {
Value::cons(entry, filtered_history)
} else {
filtered_history
};
eval.push_specpdl_root(updated_history);
eval.set_variable("load-history", updated_history);
if entire {
eval.set_variable("current-load-list", Value::T);
}
eval.restore_specpdl_roots(roots);
}
fn reverse_copy_rooted_list(eval: &mut super::eval::Context, list: Value) -> Value {
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(list);
let mut tail = list;
let mut reversed = Value::NIL;
while tail.is_cons() {
let iter_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(reversed);
reversed = Value::cons(tail.cons_car(), reversed);
eval.restore_specpdl_roots(iter_roots);
tail = tail.cons_cdr();
}
eval.restore_specpdl_roots(roots);
reversed
}
fn filter_load_history_without_filename(
eval: &mut super::eval::Context,
history: Value,
filename: &LispString,
) -> Value {
let roots = eval.save_specpdl_roots();
eval.push_specpdl_root(history);
let mut tail = history;
let mut filtered_reversed = Value::NIL;
while tail.is_cons() {
let existing = tail.cons_car();
let keep = if existing.is_cons() {
existing
.cons_car()
.as_lisp_string()
.is_none_or(|loaded| !load_name_equal(loaded, filename))
} else {
true
};
if keep {
let iter_roots = eval.save_specpdl_roots();
eval.push_specpdl_root(filtered_reversed);
filtered_reversed = Value::cons(existing, filtered_reversed);
eval.restore_specpdl_roots(iter_roots);
}
tail = tail.cons_cdr();
}
eval.push_specpdl_root(filtered_reversed);
let filtered = reverse_copy_rooted_list(eval, filtered_reversed);
eval.restore_specpdl_roots(roots);
filtered
}
fn run_after_load_evaluation(eval: &mut super::eval::Context, path_lisp: &LispString) {
let path_str = load_display_string(path_lisp);
let roots = eval.save_specpdl_roots();
let dale_id = super::intern::intern("do-after-load-evaluation");
let is_fboundp = eval
.obarray()
.symbol_function_id(dale_id)
.is_some_and(|f| !f.is_nil());
if is_fboundp {
let abs_path = Value::heap_string(path_lisp.clone());
eval.push_specpdl_root(abs_path);
if let Err(e) = eval.apply1(Value::symbol(dale_id), abs_path) {
let err_msg = match &e {
super::error::Flow::Signal(sig) => {
let sym = super::intern::resolve_sym(sig.symbol);
let data: Vec<String> =
sig.data.iter().map(|v| format_value_for_error(v)).collect();
format!("({} {})", sym, data.join(" "))
}
other => format!("{other:?}"),
};
tracing::warn!(
"do-after-load-evaluation error for {}: {}",
path_str,
err_msg
);
}
}
eval.restore_specpdl_roots(roots);
}
pub fn register_bootstrap_vars(obarray: &mut super::symbol::Obarray) {
obarray.set_symbol_value("after-load-alist", Value::NIL);
obarray.make_special("after-load-alist");
obarray.set_symbol_value("macroexp--dynvars", Value::NIL);
obarray.make_special("macroexp--dynvars");
}
fn normalized_bootstrap_features(extra_features: &[&str]) -> Vec<String> {
let mut features = extra_features
.iter()
.map(|feature| (*feature).to_string())
.filter(|feature| !feature.is_empty())
.collect::<Vec<_>>();
features.sort_unstable();
features.dedup();
features
}
const BOOTSTRAP_IMAGE_SCHEMA_VERSION: u32 = 19;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadupDumpMode {
Pbootstrap,
Pdump,
}
impl LoadupDumpMode {
pub const fn as_gnu_string(self) -> &'static str {
match self {
Self::Pbootstrap => "pbootstrap",
Self::Pdump => "pdump",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LoadupStartupSurface {
pub command_line_args: Vec<String>,
pub noninteractive: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeImageRole {
Bootstrap,
Final,
}
impl RuntimeImageRole {
pub const fn canonical_image_stem(self) -> &'static str {
match self {
Self::Bootstrap => "bootstrap-neomacs",
Self::Final => "neomacs",
}
}
pub const fn image_file_name(self) -> &'static str {
match self {
Self::Bootstrap => "bootstrap-neomacs.pdump",
Self::Final => "neomacs.pdump",
}
}
pub fn fingerprinted_image_file_name(self) -> String {
format!(
"{}-{}.pdump",
self.canonical_image_stem(),
super::pdump::fingerprint_hex()
)
}
}
const RUNTIME_ROOT_ENV: &str = "NEOMACS_RUNTIME_ROOT";
const BOOTSTRAP_CACHE_DIR_ENV: &str = "NEOVM_BOOTSTRAP_CACHE_DIR";
fn compile_time_project_root() -> PathBuf {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest.parent().expect("project root").to_path_buf()
}
fn is_runtime_root(path: &Path) -> bool {
path.join("lisp").is_dir() && path.join("etc").is_dir()
}
fn runtime_project_root() -> PathBuf {
if let Ok(root) = std::env::var(RUNTIME_ROOT_ENV) {
let path = PathBuf::from(root);
if is_runtime_root(&path) {
return path;
}
tracing::warn!(
"{RUNTIME_ROOT_ENV}={} does not contain lisp/ and etc/; falling back",
path.display()
);
}
let compile_root = compile_time_project_root();
if is_runtime_root(&compile_root) {
return compile_root;
}
if let Ok(exe) = std::env::current_exe()
&& let Some(prefix) = exe.parent().and_then(Path::parent)
{
for candidate in [
prefix.join("share/neomacs"),
prefix.join("Resources/neomacs"),
] {
if is_runtime_root(&candidate) {
return candidate;
}
}
}
panic!(
"Neomacs runtime root not found. Set {RUNTIME_ROOT_ENV} to a directory containing lisp/ and etc/."
);
}
fn bootstrap_cache_dir(runtime_root: &Path) -> PathBuf {
if let Ok(dir) = std::env::var(BOOTSTRAP_CACHE_DIR_ENV)
&& !dir.is_empty()
{
return PathBuf::from(dir);
}
let compile_root = compile_time_project_root();
if runtime_root == compile_root {
return compile_root.join("target");
}
if let Ok(dir) = std::env::var("XDG_CACHE_HOME")
&& !dir.is_empty()
{
return PathBuf::from(dir).join("neomacs");
}
if let Ok(home) = std::env::var("HOME")
&& !home.is_empty()
{
return PathBuf::from(home).join(".cache/neomacs");
}
std::env::temp_dir().join("neomacs")
}
fn should_hash_bootstrap_source_file(path: &Path) -> bool {
path.extension().and_then(OsStr::to_str) == Some("el")
}
fn collect_bootstrap_source_files(path: &Path, out: &mut Vec<PathBuf>) {
let Ok(metadata) = fs::metadata(path) else {
return;
};
if metadata.is_file() {
if should_hash_bootstrap_source_file(path) {
out.push(path.to_path_buf());
}
return;
}
let Ok(entries) = fs::read_dir(path) else {
return;
};
let mut children = entries
.filter_map(|entry| entry.ok().map(|e| e.path()))
.collect::<Vec<_>>();
children.sort();
for child in children {
collect_bootstrap_source_files(&child, out);
}
}
fn bootstrap_source_fingerprint(runtime_root: &Path) -> String {
let mut files = Vec::new();
collect_bootstrap_source_files(&runtime_root.join("lisp"), &mut files);
files.sort();
let mut hasher = Sha256::new();
hasher.update(b"neomacs-bootstrap-source-fingerprint-v1\0");
for path in files {
let rel = path.strip_prefix(runtime_root).unwrap_or(&path);
hasher.update(rel.as_os_str().as_encoded_bytes());
hasher.update([0]);
match fs::read(&path) {
Ok(bytes) => {
hasher.update([1]);
hasher.update(bytes);
}
Err(err) => {
hasher.update([0]);
hasher.update(err.to_string().as_bytes());
}
}
hasher.update([0xff]);
}
let digest = hasher.finalize();
format!("{:x}", digest)[..16].to_string()
}
fn bootstrap_dump_path(runtime_root: &Path, extra_features: &[&str]) -> PathBuf {
let features = normalized_bootstrap_features(extra_features);
let suffix = if features.is_empty() {
String::new()
} else {
format!("-{}", features.join("-"))
};
let source_fingerprint = bootstrap_source_fingerprint(runtime_root);
bootstrap_cache_dir(runtime_root).join(format!(
"neovm-bootstrap-v{BOOTSTRAP_IMAGE_SCHEMA_VERSION}-{source_fingerprint}{suffix}.pdump"
))
}
fn runtime_image_stem_for_executable(executable: &Path, role: RuntimeImageRole) -> String {
let file_name = executable
.file_name()
.and_then(OsStr::to_str)
.unwrap_or(role.canonical_image_stem());
file_name
.strip_suffix(".exe")
.unwrap_or(file_name)
.to_string()
}
pub fn runtime_image_path_for_executable(executable: &Path, role: RuntimeImageRole) -> PathBuf {
executable
.parent()
.unwrap_or_else(|| Path::new("."))
.join(format!(
"{}.pdump",
runtime_image_stem_for_executable(executable, role)
))
}
pub fn fingerprinted_runtime_image_path_for_executable(
executable: &Path,
role: RuntimeImageRole,
) -> PathBuf {
executable
.parent()
.unwrap_or_else(|| Path::new("."))
.join(role.fingerprinted_image_file_name())
}
pub fn default_runtime_image_path(role: RuntimeImageRole) -> PathBuf {
let executable = std::env::current_exe()
.ok()
.and_then(|path| path.canonicalize().ok().or(Some(path)))
.unwrap_or_else(|| PathBuf::from(role.image_file_name()));
runtime_image_path_for_executable(&executable, role)
}
fn default_fingerprinted_runtime_image_path(role: RuntimeImageRole) -> PathBuf {
let executable = std::env::current_exe()
.ok()
.and_then(|path| path.canonicalize().ok().or(Some(path)))
.unwrap_or_else(|| PathBuf::from(role.image_file_name()));
fingerprinted_runtime_image_path_for_executable(&executable, role)
}
fn runtime_image_candidate_paths_for_executable(
executable: &Path,
role: RuntimeImageRole,
) -> Vec<PathBuf> {
let primary = runtime_image_path_for_executable(executable, role);
let fingerprinted = fingerprinted_runtime_image_path_for_executable(executable, role);
if primary == fingerprinted {
vec![primary]
} else {
vec![primary, fingerprinted]
}
}
fn bootstrap_dump_lock_path(dump_path: &Path) -> PathBuf {
let file_name = dump_path
.file_name()
.expect("bootstrap dump path should have file name");
let mut lock_name = file_name.to_os_string();
lock_name.push(".lock");
dump_path.with_file_name(lock_name)
}
#[derive(Debug)]
enum BootstrapCacheLockError {
Busy(String),
Other(String),
}
impl std::fmt::Display for BootstrapCacheLockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Busy(message) | Self::Other(message) => f.write_str(message),
}
}
}
fn open_bootstrap_lock_file(lock_path: &Path) -> Result<std::fs::File, String> {
if let Some(parent) = lock_path.parent()
&& !parent.exists()
{
std::fs::create_dir_all(parent).map_err(|err| {
format!(
"bootstrap cache lock: failed creating {}: {err}",
parent.display()
)
})?;
}
std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(lock_path)
.map_err(|err| {
format!(
"bootstrap cache lock: failed opening {}: {err}",
lock_path.display()
)
})
}
struct BootstrapCacheWriteLock {
#[cfg(unix)]
file: std::fs::File,
}
impl BootstrapCacheWriteLock {
fn acquire(lock_path: &Path) -> Result<Self, BootstrapCacheLockError> {
#[cfg(unix)]
{
let file =
open_bootstrap_lock_file(lock_path).map_err(BootstrapCacheLockError::Other)?;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc != 0 {
let err = std::io::Error::last_os_error();
if matches!(err.raw_os_error(), Some(libc::EWOULDBLOCK)) {
return Err(BootstrapCacheLockError::Busy(format!(
"bootstrap cache lock busy at {}",
lock_path.display()
)));
}
return Err(BootstrapCacheLockError::Other(format!(
"bootstrap cache lock: failed locking {}: {}",
lock_path.display(),
err
)));
}
Ok(Self { file })
}
#[cfg(not(unix))]
{
let _ = lock_path;
Ok(Self {})
}
}
}
struct BootstrapCacheReadLock {
#[cfg(unix)]
file: std::fs::File,
}
impl BootstrapCacheReadLock {
fn wait(lock_path: &Path) -> Result<Self, String> {
#[cfg(unix)]
{
let file = open_bootstrap_lock_file(lock_path)?;
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_SH) };
if rc != 0 {
let err = std::io::Error::last_os_error();
return Err(format!(
"bootstrap cache lock: failed waiting on {}: {}",
lock_path.display(),
err
));
}
Ok(Self { file })
}
#[cfg(not(unix))]
{
let _ = lock_path;
Ok(Self {})
}
}
}
#[cfg(unix)]
impl Drop for BootstrapCacheWriteLock {
fn drop(&mut self) {
let _ = unsafe { libc::flock(self.file.as_raw_fd(), libc::LOCK_UN) };
}
}
fn ensure_startup_compat_variables(eval: &mut super::eval::Context, project_root: &Path) {
let etc_dir = format!("{}/", project_root.join("etc").to_string_lossy());
let source_dir = format!("{}/", project_root.to_string_lossy());
let temporary_file_directory = std::env::temp_dir().to_string_lossy().to_string();
let path_separator = if cfg!(windows) { ";" } else { ":" };
let process_environment = Value::list(
std::env::vars()
.map(|(name, value)| Value::string(format!("{name}={value}")))
.collect::<Vec<_>>(),
);
{
let obarray = eval.obarray_mut();
obarray.make_special("initial-environment");
obarray.make_special("process-environment");
}
eval.set_variable("initial-environment", process_environment.clone());
eval.set_variable("process-environment", process_environment.clone());
let system_name = super::builtins_extra::builtin_system_name(vec![])
.unwrap_or_else(|_| Value::string("localhost"));
let user_full_name = super::builtins_extra::builtin_user_full_name(vec![])
.unwrap_or_else(|_| Value::string("unknown"));
let user_login_name = super::builtins_extra::builtin_user_login_name(vec![])
.unwrap_or_else(|_| Value::string("unknown"));
let user_real_login_name = super::builtins_extra::builtin_user_real_login_name(vec![])
.unwrap_or_else(|_| Value::string("unknown"));
let system_configuration = super::builtins_extra::system_configuration_value();
let system_configuration_options = super::builtins_extra::system_configuration_options_value();
let system_configuration_features =
super::builtins_extra::system_configuration_features_value();
let operating_system_release = super::builtins_extra::operating_system_release_value();
let defaults = [
(
"emacs-copyright",
Value::string("Copyright (C) 2026 Free Software Foundation, Inc."),
),
("data-directory", Value::unibyte_string(etc_dir.clone())),
("doc-directory", Value::unibyte_string(etc_dir)),
(
"source-directory",
Value::unibyte_string(source_dir.clone()),
),
("installation-directory", Value::unibyte_string(source_dir)),
("exec-directory", Value::NIL),
("configure-info-directory", Value::NIL),
("charset-map-path", Value::NIL),
("path-separator", Value::string(path_separator)),
("file-name-coding-system", Value::NIL),
("default-file-name-coding-system", Value::NIL),
("set-auto-coding-function", Value::NIL),
("after-insert-file-functions", Value::NIL),
("write-region-annotate-functions", Value::NIL),
("write-region-post-annotation-function", Value::NIL),
("write-region-annotations-so-far", Value::NIL),
("inhibit-file-name-handlers", Value::NIL),
("inhibit-file-name-operation", Value::NIL),
(
"temporary-file-directory",
Value::string(temporary_file_directory),
),
("system-uses-terminfo", Value::T),
("create-lockfiles", Value::T),
("auto-save-list-file-name", Value::NIL),
("auto-save-list-file-prefix", Value::NIL),
("auto-save-visited-file-name", Value::NIL),
("auto-save-include-big-deletions", Value::NIL),
("shared-game-score-directory", Value::NIL),
("invocation-name", Value::NIL),
("invocation-directory", Value::NIL),
("system-messages-locale", Value::NIL),
("system-time-locale", Value::NIL),
("before-init-time", Value::NIL),
("after-init-time", Value::NIL),
("system-configuration", system_configuration),
("system-configuration-options", system_configuration_options),
(
"system-configuration-features",
system_configuration_features,
),
("system-name", system_name),
("user-full-name", user_full_name),
("user-login-name", user_login_name),
("user-real-login-name", user_real_login_name),
("operating-system-release", operating_system_release),
("delayed-warnings-list", Value::NIL),
("default-text-properties", Value::NIL),
("char-property-alias-alist", Value::NIL),
("inhibit-point-motion-hooks", Value::T),
(
"text-property-default-nonsticky",
Value::list(vec![
Value::cons(Value::symbol("syntax-table"), Value::T),
Value::cons(Value::symbol("display"), Value::T),
]),
),
];
for (name, value) in defaults {
if eval.obarray().symbol_value(name).is_none() {
eval.set_variable(name, value);
}
}
crate::emacs_core::xfaces::ensure_startup_compat_variables(eval);
}
fn value_symbol_name(value: Value) -> Option<String> {
if let Some(name) = value.as_symbol_name() {
return Some(name.to_owned());
}
value_quoted_symbol_name(value)
}
fn value_quoted_symbol_name(value: Value) -> Option<String> {
if let Some(name) = value.as_symbol_name() {
return Some(name.to_owned());
}
let items = list_to_vec(&value)?;
if items.len() == 2 {
if items[0].is_symbol_named("quote") {
return items[1].as_symbol_name().map(|s| s.to_owned());
}
}
None
}
fn value_runtime_literal(value: Value) -> Option<Value> {
if !value.is_cons() {
return Some(value);
}
value_quoted_symbol_name(value).map(|name| Value::symbol(&name))
}
#[derive(Default)]
struct LoaddefsSurfaceState {
names: std::collections::BTreeSet<String>,
autoload_args: Vec<Vec<Value>>,
property_forms: Vec<Value>,
property_keys: std::collections::BTreeSet<(String, String)>,
keymap_defvar_forms: Vec<Value>,
keymap_forms: Vec<LoaddefsKeymapReplayForm>,
}
struct LoaddefsKeymapReplayForm {
target: String,
form: Value,
}
#[derive(Default)]
struct SourceFileSurfaceState {
function_names: std::collections::BTreeSet<String>,
variable_names: std::collections::BTreeSet<String>,
face_names: std::collections::BTreeSet<String>,
property_keys: std::collections::BTreeSet<(String, String)>,
features: std::collections::BTreeSet<String>,
}
fn source_surface_insert_property(
state: &mut SourceFileSurfaceState,
name: impl Into<String>,
prop: impl Into<String>,
) {
state.property_keys.insert((name.into(), prop.into()));
}
fn collect_source_surface(form: Value, state: &mut SourceFileSurfaceState) {
let Some(items) = list_to_vec(&form) else {
return;
};
let Some(head) = items.first() else {
return;
};
let Some(head_name) = head.as_symbol_name() else {
return;
};
match head_name {
"progn" | "eval-and-compile" => {
for item in items.iter().skip(1) {
collect_source_surface(*item, state);
}
}
"defun" | "defmacro" | "defsubst" | "define-inline" => {
if let Some(name) = items.get(1).and_then(|v| value_symbol_name(*v)) {
state.function_names.insert(name);
}
}
"defalias" => {
if let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) {
state.function_names.insert(name);
}
}
"defvar" | "defconst" | "defcustom" => {
if let Some(name) = items.get(1).and_then(|v| value_symbol_name(*v)) {
state.variable_names.insert(name);
}
}
"defface" => {
if let Some(name) = items.get(1).and_then(|v| value_symbol_name(*v)) {
state.variable_names.insert(name.clone());
state.face_names.insert(name);
}
}
"put" | "function-put" | "define-symbol-prop" => {
if let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v))
&& let Some(prop) = items.get(2).and_then(|v| value_symbol_name(*v))
{
source_surface_insert_property(state, name, prop);
}
}
"def-edebug-elem-spec" => {
if let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) {
source_surface_insert_property(state, name, "edebug-form-spec");
}
}
"provide" => {
if let Some(feature) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) {
state.features.insert(feature);
}
}
"pcase-defmacro" => {
if let Some(name) = items.get(1).and_then(|v| value_symbol_name(*v)) {
let macroexpander = format!("{name}--pcase-macroexpander");
state.function_names.insert(macroexpander.clone());
source_surface_insert_property(state, ¯oexpander, "edebug-form-spec");
source_surface_insert_property(state, name, "pcase-macroexpander");
}
}
"define-icon" => {
if let Some(name) = items.get(1).and_then(|v| value_symbol_name(*v)) {
source_surface_insert_property(state, name, "icon--properties");
}
}
_ => {}
}
}
fn collect_source_surface_from_paths(
paths: &[PathBuf],
error_context: &str,
) -> Result<SourceFileSurfaceState, EvalError> {
let mut state = SourceFileSurfaceState::default();
for path in paths {
let bytes = fs::read(path).map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"{error_context}: failed reading {}: {err}",
path.display()
))],
raw_data: None,
})?;
let source = decode_emacs_utf8(&bytes);
let forms = crate::emacs_core::value_reader::read_all_with_source_multibyte(&source, true)
.map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"{error_context}: failed parsing {}: {err}",
path.display()
))],
raw_data: None,
})?;
for form in forms {
collect_source_surface(form, &mut state);
}
}
Ok(state)
}
fn collect_loaddefs_autoload_args(
expr: Value,
allowed_files: Option<&std::collections::BTreeSet<String>>,
allowed_names: Option<&std::collections::BTreeSet<String>>,
state: &mut LoaddefsSurfaceState,
) {
let Some(items) = list_to_vec(&expr) else {
return;
};
let Some(head) = items.first() else {
return;
};
if !head.is_symbol_named("autoload") {
return;
}
let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) else {
return;
};
let Some(file_value) = items.get(2).and_then(|v| value_runtime_literal(*v)) else {
return;
};
let ValueKind::String = file_value.kind() else {
return;
};
let file = load_string_text(&file_value).expect("checked string");
if let Some(files) = allowed_files
&& !files.contains(&file)
{
return;
};
if let Some(names) = allowed_names
&& !names.contains(&name)
{
return;
}
state.names.insert(name.clone());
let mut args = vec![Value::symbol(&name), file_value];
for item in items.iter().skip(3).take(3) {
let Some(value) = value_runtime_literal(*item) else {
return;
};
args.push(value);
}
state.autoload_args.push(args);
}
fn collect_loaddefs_property_forms(
expr: Value,
names: &std::collections::BTreeSet<String>,
state: &mut LoaddefsSurfaceState,
) {
let Some(items) = list_to_vec(&expr) else {
return;
};
let Some(head) = items.first() else {
return;
};
let Some(head_name) = head.as_symbol_name() else {
return;
};
if head_name != "function-put" && head_name != "put" {
return;
}
let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) else {
return;
};
if names.contains(&name) {
state.property_forms.push(expr);
if let Some(prop) = items.get(2).and_then(|v| value_symbol_name(*v)) {
state.property_keys.insert((name, prop));
}
}
}
fn loaddefs_keymap_replay_target(expr: Value) -> Option<String> {
let items = list_to_vec(&expr)?;
let head = items.first()?;
match head.as_symbol_name()? {
"define-key" | "keymap-set" => items.get(1).and_then(|value| value_symbol_name(*value)),
"if" | "progn" | "unless" | "when" => items
.iter()
.skip(1)
.find_map(|value| loaddefs_keymap_replay_target(*value)),
_ => None,
}
}
fn collect_loaddefs_keymap_forms(expr: Value, state: &mut LoaddefsSurfaceState) {
let Some(target) = loaddefs_keymap_replay_target(expr) else {
return;
};
state
.keymap_forms
.push(LoaddefsKeymapReplayForm { target, form: expr });
}
fn collect_loaddefs_keymap_defvar_form(expr: Value, state: &mut LoaddefsSurfaceState) {
let Some(items) = list_to_vec(&expr) else {
return;
};
let Some(head) = items.first() else {
return;
};
if !head.is_symbol_named("defvar") {
return;
}
let Some(name) = items.get(1).and_then(|value| value_symbol_name(*value)) else {
return;
};
if !name.ends_with("-map") {
return;
}
state.keymap_defvar_forms.push(expr);
}
fn collect_loaddefs_surface_from_paths(
paths: &[PathBuf],
allowed_files: Option<&std::collections::BTreeSet<String>>,
allowed_names: Option<&std::collections::BTreeSet<String>>,
error_context: &str,
) -> Result<LoaddefsSurfaceState, EvalError> {
let mut state = LoaddefsSurfaceState::default();
for path in paths {
let bytes = fs::read(path).map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"{error_context}: failed reading {}: {err}",
path.display()
))],
raw_data: None,
})?;
let source = decode_emacs_utf8(&bytes);
let forms = crate::emacs_core::value_reader::read_all_with_source_multibyte(&source, true)
.map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"{error_context}: failed parsing {}: {err}",
path.display()
))],
raw_data: None,
})?;
for form in &forms {
collect_loaddefs_autoload_args(*form, allowed_files, allowed_names, &mut state);
collect_loaddefs_keymap_defvar_form(*form, &mut state);
collect_loaddefs_keymap_forms(*form, &mut state);
}
let property_names = allowed_names
.cloned()
.unwrap_or_else(|| state.names.clone());
for form in &forms {
collect_loaddefs_property_forms(*form, &property_names, &mut state);
}
}
Ok(state)
}
fn compile_only_cl_loaddefs_state(project_root: &Path) -> Result<LoaddefsSurfaceState, EvalError> {
collect_loaddefs_surface_from_paths(
&[project_root.join("lisp/emacs-lisp/cl-loaddefs.el")],
None,
None,
"bootstrap runtime cleanup",
)
}
fn runtime_loaddefs_restore_state(project_root: &Path) -> Result<LoaddefsSurfaceState, EvalError> {
let runtime_files = ["gv", "icons", "pcase"]
.into_iter()
.map(str::to_string)
.collect::<std::collections::BTreeSet<_>>();
collect_loaddefs_surface_from_paths(
&[project_root.join("lisp/ldefs-boot.el")],
Some(&runtime_files),
None,
"bootstrap runtime cleanup",
)
}
fn loaded_source_paths(eval: &mut super::eval::Context) -> Vec<PathBuf> {
{
let history = eval
.obarray()
.symbol_value("load-history")
.cloned()
.unwrap_or(Value::NIL);
let mut paths = std::collections::BTreeSet::new();
for entry in list_to_vec(&history).unwrap_or_default() {
if !entry.is_cons() {
continue;
};
let Some(path) = entry.cons_car().as_lisp_string() else {
continue;
};
let path = load_path_buf(path);
if path.extension().is_some_and(|ext| ext == "el") {
paths.insert(path);
continue;
}
if path.extension().is_some_and(|ext| ext == "elc") {
let source_path = path.with_extension("el");
if source_path.exists() {
paths.insert(source_path);
}
}
}
paths.into_iter().collect()
}
}
fn is_compile_only_loaddefs_provider(path: &Path) -> bool {
matches!(
path.file_stem().and_then(|stem| stem.to_str()),
Some(
"cl-loaddefs"
| "cl-preloaded"
| "cl-lib"
| "cl-macs"
| "cl-seq"
| "cl-extra"
| "gv"
| "icons"
)
)
}
fn is_generated_loaddefs_provider(path: &Path) -> bool {
matches!(
path.file_stem().and_then(|stem| stem.to_str()),
Some("loaddefs" | "ldefs-boot" | "theme-loaddefs")
)
}
fn runtime_loaded_source_restore_state(
eval: &mut super::eval::Context,
project_root: &Path,
allowed_names: &std::collections::BTreeSet<String>,
) -> Result<LoaddefsSurfaceState, EvalError> {
let paths = loaded_source_paths(eval)
.into_iter()
.filter(|path| path.starts_with(project_root))
.filter(|path| !is_compile_only_loaddefs_provider(path))
.filter(|path| !is_generated_loaddefs_provider(path))
.collect::<Vec<_>>();
collect_loaddefs_surface_from_paths(
&paths,
None,
Some(allowed_names),
"bootstrap runtime cleanup",
)
}
fn runtime_source_bootstrap_surface_state(
project_root: &Path,
) -> Result<SourceFileSurfaceState, EvalError> {
collect_source_surface_from_paths(
&[
project_root.join("lisp/emacs-lisp/icons.el"),
project_root.join("lisp/emacs-lisp/pcase.el"),
],
"bootstrap runtime cleanup",
)
}
pub(crate) fn apply_ldefs_boot_autoloads_for_names(
eval: &mut super::eval::Context,
names: &[&str],
) -> Result<(), EvalError> {
let project_root = runtime_project_root();
let ldefs_path = project_root.join("lisp/ldefs-boot.el");
let source = fs::read_to_string(&ldefs_path).map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"ldefs-boot autoload restore: failed reading {}: {err}",
ldefs_path.display()
))],
raw_data: None,
})?;
let forms = crate::emacs_core::value_reader::read_all_with_source_multibyte(&source, true)
.map_err(|err| EvalError::Signal {
symbol: intern("error"),
data: vec![Value::string(format!(
"ldefs-boot autoload restore: failed parsing {}: {err}",
ldefs_path.display()
))],
raw_data: None,
})?;
let wanted = names
.iter()
.map(|name| (*name).to_string())
.collect::<std::collections::BTreeSet<_>>();
let roots = eval.save_specpdl_roots();
for form in &forms {
eval.push_specpdl_root(*form);
}
let result: Result<(), EvalError> = (|| {
for form in &forms {
let Some(items) = list_to_vec(form) else {
continue;
};
let Some(head) = items.first() else {
continue;
};
if head.is_symbol_named("autoload")
&& let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v))
&& wanted.contains(&name)
{
eval_generated_loaddefs_form(eval, *form)?;
}
}
let mut property_forms: Vec<Value> = Vec::new();
for form in &forms {
let Some(items) = list_to_vec(form) else {
continue;
};
let Some(head) = items.first() else {
continue;
};
let Some(head_name) = head.as_symbol_name() else {
continue;
};
if head_name != "function-put" && head_name != "put" {
continue;
}
let Some(name) = items.get(1).and_then(|v| value_quoted_symbol_name(*v)) else {
continue;
};
if wanted.contains(&name) {
property_forms.push(*form);
}
}
for form in &property_forms {
eval_generated_loaddefs_form(eval, *form)?;
}
Ok(())
})();
eval.restore_specpdl_roots(roots);
result?;
Ok(())
}
fn normalize_bootstrap_runtime_surface(
eval: &mut super::eval::Context,
project_root: &Path,
) -> Result<(), EvalError> {
let compile_only_state = compile_only_cl_loaddefs_state(project_root).map_err(|e| {
tracing::error!("compile_only_cl_loaddefs_state failed: {e:?}");
e
})?;
let runtime_loaddefs_state = runtime_loaddefs_restore_state(project_root).map_err(|e| {
tracing::error!("runtime_loaddefs_restore_state failed: {e:?}");
e
})?;
let runtime_source_state =
runtime_source_bootstrap_surface_state(project_root).map_err(|e| {
tracing::error!("runtime_source_bootstrap_surface_state failed: {e:?}");
e
})?;
let runtime_loaded_state =
runtime_loaded_source_restore_state(eval, project_root, &compile_only_state.names)
.map_err(|e| {
tracing::error!("runtime_loaded_source_restore_state failed: {e:?}");
e
})?;
let mut strip_names = compile_only_state.names.clone();
strip_names.extend(runtime_loaddefs_state.names.iter().cloned());
strip_names.extend(runtime_loaded_state.names.iter().cloned());
let mut stripped_features = TRANSIENT_RUNTIME_FEATURES
.iter()
.map(|name| (*name).to_string())
.collect::<std::collections::BTreeSet<_>>();
stripped_features.extend(runtime_source_state.features.iter().cloned());
for feature in &stripped_features {
eval.remove_feature(feature);
}
for feature in TRANSIENT_RUNTIME_FEATURES {
eval.remove_feature(feature);
}
eval.set_variable("gensym-counter", Value::fixnum(0));
for (name, prop) in compile_only_state
.property_keys
.iter()
.chain(runtime_loaddefs_state.property_keys.iter())
.chain(runtime_loaded_state.property_keys.iter())
.chain(runtime_source_state.property_keys.iter())
{
let _ = super::builtins::builtin_put(
eval,
vec![Value::symbol(name), Value::symbol(prop), Value::NIL],
);
}
for name in &strip_names {
eval.obarray_mut().fmakunbound(&name);
eval.autoloads.remove(name);
let _ = super::builtins::builtin_put(
eval,
vec![
Value::symbol(name),
Value::symbol("autoload-macro"),
Value::NIL,
],
);
}
for name in &runtime_source_state.function_names {
eval.obarray_mut().fmakunbound(name);
eval.autoloads.remove(name);
let _ = super::builtins::builtin_put(
eval,
vec![
Value::symbol(name),
Value::symbol("autoload-macro"),
Value::NIL,
],
);
}
for name in &runtime_source_state.variable_names {
eval.obarray_mut().makunbound(name);
}
for name in &runtime_source_state.face_names {
super::font::clear_created_lisp_face(name);
}
let autoload_entries = eval.autoloads.entries_snapshot();
for (name, _) in &autoload_entries {
if strip_names.contains(name) {
eval.autoloads.remove(name);
let _ = super::builtins::builtin_put(
eval,
vec![
Value::symbol(name),
Value::symbol("autoload-macro"),
Value::NIL,
],
);
}
}
let roots = eval.save_specpdl_roots();
for args in runtime_loaded_state
.autoload_args
.iter()
.chain(runtime_loaddefs_state.autoload_args.iter())
{
for v in args {
eval.push_specpdl_root(*v);
}
}
for form in runtime_loaded_state
.keymap_defvar_forms
.iter()
.chain(runtime_loaddefs_state.keymap_defvar_forms.iter())
{
eval.push_specpdl_root(*form);
}
for form in runtime_loaded_state
.property_forms
.iter()
.chain(runtime_loaddefs_state.property_forms.iter())
{
eval.push_specpdl_root(*form);
}
for replay in runtime_loaded_state
.keymap_forms
.iter()
.chain(runtime_loaddefs_state.keymap_forms.iter())
{
eval.push_specpdl_root(replay.form);
}
let result: Result<(), EvalError> = (|| {
let replay_ldefs_boot_keymaps = eval.feature_present("window");
for args in runtime_loaded_state
.autoload_args
.iter()
.chain(runtime_loaddefs_state.autoload_args.iter())
{
super::autoload::builtin_autoload(eval, args.clone()).map_err(map_flow)?;
}
if replay_ldefs_boot_keymaps {
for form in runtime_loaded_state
.keymap_defvar_forms
.iter()
.chain(runtime_loaddefs_state.keymap_defvar_forms.iter())
{
eval_runtime_form(eval, *form)?;
}
}
for form in runtime_loaded_state
.property_forms
.iter()
.chain(runtime_loaddefs_state.property_forms.iter())
{
eval_runtime_form(eval, *form)?;
}
if replay_ldefs_boot_keymaps {
for replay in runtime_loaded_state
.keymap_forms
.iter()
.chain(runtime_loaddefs_state.keymap_forms.iter())
{
let Some(value) = eval.obarray().symbol_value(&replay.target) else {
continue;
};
if !is_list_keymap(value) {
continue;
}
eval_runtime_form(eval, replay.form)?;
}
}
Ok(())
})();
eval.restore_specpdl_roots(roots);
result?;
Ok(())
}
fn bootstrap_runtime_window_system_symbol(eval: &mut super::eval::Context) -> Option<Value> {
if eval.feature_present("neomacs")
|| eval.feature_present(super::display::gui_window_system_symbol())
{
Some(Value::symbol(super::display::gui_window_system_symbol()))
} else if eval.feature_present("x") {
Some(Value::symbol("x"))
} else {
None
}
}
fn restore_cached_runtime_window_system_surface(eval: &mut super::eval::Context) {
let Some(window_system) = bootstrap_runtime_window_system_symbol(eval) else {
return;
};
let frame_id = if let Some(frame_id) = eval.frames.selected_frame().map(|frame| frame.id) {
Some(frame_id)
} else if let Some(frame_id) = eval.frames.frame_list().into_iter().next() {
let _ = eval.frames.select_frame(frame_id);
eval.sync_keyboard_terminal_owner();
Some(frame_id)
} else {
None
};
if let Some(frame_id) = frame_id
&& let Some(frame) = eval.frames.get_mut(frame_id)
{
frame.set_window_system(Some(window_system));
if frame.parameter("display-type").is_none() {
frame.set_parameter(Value::symbol("display-type"), Value::symbol("color"));
}
if frame.parameter("background-mode").is_none() {
frame.set_parameter(Value::symbol("background-mode"), Value::symbol("light"));
}
}
eval.set_variable("window-system", window_system);
eval.set_variable("initial-window-system", window_system);
}
fn clear_runtime_loader_state(eval: &mut super::eval::Context) {
eval.require_stack.clear();
eval.loads_in_progress.clear();
}
fn finalize_cached_bootstrap_eval(
eval: &mut super::eval::Context,
project_root: &Path,
) -> Result<(), EvalError> {
super::builtins::init_builtins(eval);
{
use crate::buffer::buffer::BUFFER_SLOT_INFO;
use crate::emacs_core::forward::alloc_buffer_objfwd;
use crate::emacs_core::intern::intern;
let obarray = eval.obarray_mut();
for info in BUFFER_SLOT_INFO {
if !info.install_as_forwarder {
continue;
}
let id = intern(info.name);
let predicate = if info.predicate.is_empty() {
intern("null")
} else {
intern(info.predicate)
};
let fwd = alloc_buffer_objfwd(
info.offset as u16,
info.local_flags_idx,
predicate,
info.default.to_value(),
);
obarray.install_buffer_objfwd(id, fwd);
}
}
super::font::restore_created_faces_from_table(&eval.face_table.face_list());
clear_runtime_loader_state(eval);
ensure_startup_compat_variables(eval, project_root);
restore_cached_runtime_window_system_surface(eval);
let lisp_dir = project_root.join("lisp");
eval.set_variable(
"load-path",
Value::list(bootstrap_load_path_entries(&lisp_dir)),
);
let etc_dir = project_root.join("etc");
eval.set_variable(
"data-directory",
Value::unibyte_string(format!("{}/", etc_dir.to_string_lossy())),
);
eval.set_variable(
"source-directory",
Value::unibyte_string(format!("{}/", project_root.to_string_lossy())),
);
eval.set_variable(
"installation-directory",
Value::unibyte_string(format!("{}/", project_root.to_string_lossy())),
);
if let Ok(cwd) = std::env::current_dir() {
let mut cwd_string = cwd.to_string_lossy().into_owned();
if !cwd_string.ends_with('/') {
cwd_string.push('/');
}
eval.set_variable("default-directory", Value::unibyte_string(cwd_string));
}
eval.set_variable("abbreviated-home-dir", Value::NIL);
eval.clear_top_level_eval_state();
Ok(())
}
pub(crate) fn bootstrap_load_path_entries(lisp_dir: &Path) -> Vec<Value> {
let mut load_path_entries = Vec::new();
for sub in BOOTSTRAP_LOAD_PATH_SUBDIRS {
let dir = if sub.is_empty() {
lisp_dir.to_path_buf()
} else {
lisp_dir.join(sub)
};
if dir.is_dir() {
load_path_entries.push(Value::string(dir.to_string_lossy().to_string()));
}
}
load_path_entries
}
pub(crate) fn runtime_bootstrap_load_path() -> Vec<String> {
let lisp_dir = runtime_project_root().join("lisp");
bootstrap_load_path_entries(&lisp_dir)
.into_iter()
.filter_map(|value| {
value
.is_string()
.then(|| load_string_text(&value).expect("checked string"))
})
.collect()
}
fn eval_startup_forms(eval: &mut super::eval::Context, forms_src: &str) -> Result<(), EvalError> {
eval.eval_str(forms_src)?;
Ok(())
}
fn sync_runtime_interpreted_closure_filter(eval: &mut super::eval::Context) {
let closure_filter_sym = super::intern::intern("internal-make-interpreted-closure-function");
let cconv_sym = super::intern::intern("cconv-make-interpreted-closure");
let filter_fn = eval
.obarray()
.symbol_value_id(closure_filter_sym)
.cloned()
.and_then(|value| {
if value.as_symbol_id() == Some(cconv_sym) {
eval.obarray().symbol_function_id(cconv_sym)
} else {
None
}
});
eval.set_interpreted_closure_filter_fn(filter_fn);
}
pub fn apply_runtime_startup_state(eval: &mut super::eval::Context) -> Result<(), EvalError> {
let project_root = runtime_project_root();
eval_startup_forms(
eval,
r#"
(if (get-buffer "*scratch*")
(with-current-buffer "*scratch*"
(if (eq major-mode 'fundamental-mode)
(funcall initial-major-mode))))
"#,
)?;
normalize_bootstrap_runtime_surface(eval, &project_root)?;
sync_runtime_interpreted_closure_filter(eval);
for feature in TRANSIENT_RUNTIME_FEATURES {
eval.remove_feature(feature);
}
eval.clear_top_level_eval_state();
Ok(())
}
fn install_bootstrap_x_window_system_vars(
eval: &mut super::eval::Context,
) -> Result<(), EvalError> {
let keysym_table = builtin_make_hash_table(vec![
Value::keyword(":test"),
Value::symbol("eql"),
Value::keyword(":size"),
Value::fixnum(900),
])
.map_err(map_flow)?;
eval.set_variable("x-keysym-table", keysym_table);
eval.set_variable("x-selection-timeout", Value::fixnum(0));
eval.set_variable("x-session-id", Value::NIL);
eval.set_variable("x-session-previous-id", Value::NIL);
for name in [
"x-ctrl-keysym",
"x-alt-keysym",
"x-hyper-keysym",
"x-meta-keysym",
"x-super-keysym",
] {
eval.set_variable(name, Value::NIL);
}
Ok(())
}
fn maybe_trace_bootstrap_step(message: impl AsRef<str>) {
if std::env::var_os("NEOVM_TRACE_BOOTSTRAP_STEPS").is_some() {
eprintln!("bootstrap-step: {}", message.as_ref());
}
}
fn maybe_trace_bootstrap_macro_perf(eval: &super::eval::Context) {
if let Some(summary) = eval.macro_perf_summary() {
let gc_elapsed = eval
.obarray()
.symbol_value("gc-elapsed")
.and_then(|value| value.as_number_f64())
.unwrap_or(0.0);
eprintln!(
"bootstrap-macro-perf: {summary} | gc=gcs-done:{} elapsed:{:.3}s",
eval.gc_count, gc_elapsed
);
}
}
pub fn create_bootstrap_evaluator() -> Result<super::eval::Context, EvalError> {
create_bootstrap_evaluator_with_features(&[])
}
fn set_loadup_dump_mode(eval: &mut super::eval::Context, dump_mode: Option<LoadupDumpMode>) {
match dump_mode {
Some(mode) => eval.set_variable("dump-mode", Value::string(mode.as_gnu_string())),
None => eval.set_variable("dump-mode", Value::NIL),
}
}
fn apply_loadup_startup_surface(
eval: &mut super::eval::Context,
startup_surface: &LoadupStartupSurface,
) {
let argv = startup_surface
.command_line_args
.iter()
.cloned()
.map(Value::string)
.collect::<Vec<_>>();
eval.set_variable("command-line-args", Value::list(argv));
eval.set_variable("command-line-args-left", Value::NIL);
eval.set_variable("command-line-processed", Value::NIL);
eval.set_variable(
"noninteractive",
if startup_surface.noninteractive {
Value::T
} else {
Value::NIL
},
);
}
pub fn create_bootstrap_evaluator_with_features(
extra_features: &[&str],
) -> Result<super::eval::Context, EvalError> {
create_bootstrap_evaluator_with_dump_mode(extra_features, None)
}
pub fn create_bootstrap_evaluator_with_dump_mode(
extra_features: &[&str],
dump_mode: Option<LoadupDumpMode>,
) -> Result<super::eval::Context, EvalError> {
create_bootstrap_evaluator_with_startup_surface(extra_features, dump_mode, None)
}
pub fn create_bootstrap_evaluator_with_startup_surface(
extra_features: &[&str],
dump_mode: Option<LoadupDumpMode>,
startup_surface: Option<&LoadupStartupSurface>,
) -> Result<super::eval::Context, EvalError> {
let project_root = runtime_project_root();
let lisp_dir = project_root.join("lisp");
assert!(
lisp_dir.is_dir(),
"lisp/ directory not found at {}",
lisp_dir.display()
);
stacker::maybe_grow(128 * 1024, 2 * 1024 * 1024, || {
maybe_trace_bootstrap_step("create_bootstrap_evaluator_with_features: enter");
let mut eval = super::eval::Context::new();
maybe_trace_bootstrap_step("create_bootstrap_evaluator_with_features: evaluator-new");
let bootstrap_features = normalized_bootstrap_features(extra_features);
for feature in &bootstrap_features {
let _ = eval.provide_value(Value::symbol(&feature), None);
}
maybe_trace_bootstrap_step(format!(
"create_bootstrap_evaluator_with_features: provided-features={bootstrap_features:?}"
));
if bootstrap_features
.iter()
.any(|feature| feature == "x" || feature == "neomacs")
{
install_bootstrap_x_window_system_vars(&mut eval)?;
maybe_trace_bootstrap_step(
"create_bootstrap_evaluator_with_features: installed-x-window-system-vars",
);
}
eval.set_variable(
"load-path",
Value::list(bootstrap_load_path_entries(&lisp_dir)),
);
let bootstrap_frame_id = super::window_cmds::seed_batch_startup_frame_in_state(
&mut eval.frames,
&mut eval.buffers,
);
maybe_trace_bootstrap_step(format!(
"create_bootstrap_evaluator_with_features: seeded-batch-bootstrap-frame={bootstrap_frame_id:?}"
));
if let Some(startup_surface) = startup_surface {
apply_loadup_startup_surface(&mut eval, startup_surface);
maybe_trace_bootstrap_step(
"create_bootstrap_evaluator_with_features: applied-loadup-startup-surface",
);
}
set_loadup_dump_mode(&mut eval, dump_mode);
eval.set_variable("purify-flag", Value::NIL);
eval.set_variable("max-lisp-eval-depth", Value::fixnum(2400));
eval.set_variable("inhibit-load-charset-map", Value::T);
let etc_dir = project_root.join("etc");
eval.set_variable(
"data-directory",
Value::unibyte_string(format!("{}/", etc_dir.to_string_lossy())),
);
eval.set_variable(
"source-directory",
Value::unibyte_string(format!("{}/", project_root.to_string_lossy())),
);
eval.set_variable(
"installation-directory",
Value::unibyte_string(format!("{}/", project_root.to_string_lossy())),
);
let path_dirs: Vec<Value> = std::env::var("PATH")
.unwrap_or_default()
.split(':')
.filter(|s| !s.is_empty())
.map(|s| Value::unibyte_string(s.to_string()))
.collect();
eval.set_variable("exec-path", Value::list(path_dirs));
eval.set_variable("exec-suffixes", Value::NIL);
eval.set_variable("exec-directory", Value::NIL);
for (name, program) in [
("ctags-program-name", "ctags"),
("etags-program-name", "etags"),
("hexl-program-name", "hexl"),
("emacsclient-program-name", "emacsclient"),
("movemail-program-name", "movemail"),
("ebrowse-program-name", "ebrowse"),
("rcs2log-program-name", "rcs2log"),
] {
eval.set_variable(name, Value::unibyte_string(program));
eval.obarray.make_special(name);
}
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
eval.set_variable("shell-file-name", Value::unibyte_string(shell));
eval.set_variable("shell-command-switch", Value::unibyte_string("-c"));
eval.set_variable(
"menu-bar-final-items",
Value::list(vec![Value::symbol("help-menu")]),
);
{
let stubs = [
"(put 'glyphless-char-display 'char-table-extra-slots 1)",
"(setq glyphless-char-display (make-char-table 'glyphless-char-display nil))",
"(set-char-table-extra-slot glyphless-char-display 0 'empty-box)",
];
for stub in &stubs {
let _ = eval.eval_str(stub);
}
}
let loadup_path = lisp_dir.join("loadup.el");
tracing::info!("Loading loadup.el from {}", loadup_path.display());
let _bootstrap_ldefs_boot_preference = BootstrapLdefsBootPreferenceGuard::enable();
match load_file(&mut eval, &loadup_path) {
Ok(_) => tracing::info!("loadup.el completed successfully"),
Err(e) => {
if is_kill_emacs_signal(&e) {
tracing::info!("loadup.el completed (kill-emacs after dump)");
} else {
let rendered = format_eval_error_in_state(&eval, &e);
tracing::error!("loadup.el failed: {rendered}");
maybe_trace_bootstrap_step(format!(
"create_bootstrap_evaluator_with_features: loadup-failed={rendered}"
));
return Err(e);
}
}
}
maybe_trace_bootstrap_macro_perf(&eval);
if dump_mode.is_some() && eval.shutdown_request.is_some() {
return Ok(eval);
}
eval.shutdown_request = None;
if bootstrap_features.iter().any(|f| f == "neomacs") && !eval.feature_present("x") {
let load_path = get_load_path(&eval.obarray());
for neo_file in &["term/common-win", "term/neo-win"] {
if let Some(path) = find_file_in_load_path(neo_file, &load_path) {
tracing::info!("LOADING (neomacs): {neo_file} ...");
if let Err(e) = load_file(&mut eval, &path) {
tracing::error!("FAIL (neomacs): {neo_file} => {e:?}");
return Err(e);
}
}
}
}
tracing::info!("\n=== LOADUP BOOTSTRAP COMPLETE ===");
eval.set_lexical_binding(true);
eval.clear_top_level_eval_state();
let _ = eval.frames.delete_frame(bootstrap_frame_id);
clear_runtime_loader_state(&mut eval);
Ok(eval)
})
}
pub fn create_bootstrap_evaluator_cached() -> Result<super::eval::Context, EvalError> {
create_bootstrap_evaluator_cached_with_features(&[])
}
pub fn create_runtime_startup_evaluator() -> Result<super::eval::Context, EvalError> {
create_runtime_startup_evaluator_with_features(&[])
}
pub(crate) fn create_runtime_startup_evaluator_at_path(
extra_features: &[&str],
dump_path: &Path,
) -> Result<super::eval::Context, EvalError> {
let mut eval = create_bootstrap_evaluator_cached_at_path(extra_features, dump_path)?;
apply_runtime_startup_state(&mut eval)?;
maybe_run_after_pdump_load_hook(&mut eval);
Ok(eval)
}
pub fn create_runtime_startup_evaluator_with_features(
extra_features: &[&str],
) -> Result<super::eval::Context, EvalError> {
let project_root = runtime_project_root();
let dump_path = bootstrap_dump_path(&project_root, extra_features);
create_runtime_startup_evaluator_at_path(extra_features, &dump_path)
}
pub fn create_runtime_startup_evaluator_cached() -> Result<super::eval::Context, EvalError> {
create_runtime_startup_evaluator()
}
pub(crate) fn create_runtime_startup_evaluator_cached_at_path(
extra_features: &[&str],
dump_path: &Path,
) -> Result<super::eval::Context, EvalError> {
create_runtime_startup_evaluator_at_path(extra_features, dump_path)
}
pub fn create_runtime_startup_evaluator_cached_with_features(
extra_features: &[&str],
) -> Result<super::eval::Context, EvalError> {
create_runtime_startup_evaluator_with_features(extra_features)
}
pub fn load_runtime_image_with_features(
role: RuntimeImageRole,
extra_features: &[&str],
dump_path: Option<&Path>,
) -> Result<super::eval::Context, EvalError> {
let executable = std::env::current_exe()
.ok()
.and_then(|path| path.canonicalize().ok().or(Some(path)))
.unwrap_or_else(|| {
if dump_path.is_some() {
PathBuf::from(role.image_file_name())
} else {
default_runtime_image_path(role)
}
});
load_runtime_image_with_features_for_executable(role, extra_features, dump_path, &executable)
}
pub(crate) fn load_runtime_image_with_features_for_executable(
role: RuntimeImageRole,
extra_features: &[&str],
dump_path: Option<&Path>,
executable: &Path,
) -> Result<super::eval::Context, EvalError> {
use super::pdump;
let project_root = runtime_project_root();
let candidates = dump_path
.map(|path| vec![path.to_path_buf()])
.unwrap_or_else(|| runtime_image_candidate_paths_for_executable(executable, role));
let mut eval = {
let mut last_error = None;
let mut loaded = None;
for (index, candidate) in candidates.iter().enumerate() {
match pdump::load_from_dump(candidate) {
Ok(eval) => {
loaded = Some(eval);
break;
}
Err(pdump::DumpError::Io(err))
if err.kind() == std::io::ErrorKind::NotFound
&& index + 1 < candidates.len() =>
{
tracing::info!(
"pdump: runtime image {} not found, trying next candidate",
candidate.display()
);
}
Err(err) => {
last_error = Some(runtime_image_load_error(role, candidate, err));
break;
}
}
}
match (loaded, last_error) {
(Some(eval), _) => eval,
(None, Some(err)) => return Err(err),
(None, None) => {
return Err(runtime_image_load_error(
role,
&default_fingerprinted_runtime_image_path(role),
pdump::DumpError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"runtime image not found",
)),
));
}
}
};
if !extra_features.is_empty() {
let bootstrap_features = normalized_bootstrap_features(extra_features);
for feature in &bootstrap_features {
let _ = eval.provide_value(Value::symbol(feature), None);
}
}
finalize_cached_bootstrap_eval(&mut eval, &project_root).map_err(|e| {
tracing::error!("finalize_cached_bootstrap_eval failed: {e:?}");
e
})?;
Ok(eval)
}
fn runtime_image_load_error(
role: RuntimeImageRole,
dump_path: &Path,
err: super::pdump::DumpError,
) -> EvalError {
let image_kind = match role {
RuntimeImageRole::Bootstrap => "bootstrap",
RuntimeImageRole::Final => "final",
};
let message = format!(
"failed to load {image_kind} image {}: {err}",
dump_path.display()
);
tracing::error!("{message}");
let payload = Value::symbol(intern(&message));
EvalError::Signal {
symbol: intern("error"),
data: vec![payload],
raw_data: Some(payload),
}
}
pub fn maybe_run_after_pdump_load_hook(eval: &mut super::eval::Context) -> bool {
if super::pdump::take_after_pdump_load_hook_pending(eval) {
super::pdump::runtime::run_after_pdump_load_hook(eval);
return true;
}
false
}
pub fn create_bootstrap_evaluator_cached_with_features(
extra_features: &[&str],
) -> Result<super::eval::Context, EvalError> {
let project_root = runtime_project_root();
let dump_path = bootstrap_dump_path(&project_root, extra_features);
create_bootstrap_evaluator_cached_at_path(extra_features, &dump_path)
}
pub(crate) fn create_bootstrap_evaluator_cached_at_path(
extra_features: &[&str],
dump_path: &Path,
) -> Result<super::eval::Context, EvalError> {
use super::pdump;
fn finalize_or_log(
eval: &mut super::eval::Context,
project_root: &Path,
context: &str,
) -> Result<(), EvalError> {
match finalize_cached_bootstrap_eval(eval, project_root) {
Ok(()) => Ok(()),
Err(err) => {
let rendered = format_eval_error_in_state(eval, &err);
tracing::error!("{context}: {rendered}");
Err(err)
}
}
}
fn try_load_dump(
dump_path: &Path,
project_root: &Path,
log_context: &str,
) -> Result<Option<super::eval::Context>, EvalError> {
let start = std::time::Instant::now();
match pdump::load_from_dump(dump_path) {
Ok(mut eval) => {
tracing::info!(
"pdump: loaded bootstrap state from {} {} ({:.2?})",
dump_path.display(),
log_context,
start.elapsed()
);
finalize_or_log(
&mut eval,
project_root,
"pdump finalize after cached load failed",
)?;
Ok(Some(eval))
}
Err(err) => {
tracing::warn!(
"pdump: load {} failed ({err})",
if log_context.is_empty() {
"attempt"
} else {
log_context
}
);
Ok(None)
}
}
}
let project_root = runtime_project_root();
let lock_path = bootstrap_dump_lock_path(dump_path);
tracing::info!("pdump: bootstrap cache candidate {}", dump_path.display());
if std::env::var("NEOVM_DISABLE_PDUMP").unwrap_or_default() == "1" {
let mut eval = create_bootstrap_evaluator_with_features(extra_features)?;
finalize_or_log(&mut eval, &project_root, "pdump disabled finalize failed")?;
return Ok(eval);
}
if dump_path.exists() {
if let Some(eval) = try_load_dump(dump_path, &project_root, "on first attempt")? {
return Ok(eval);
}
} else {
tracing::info!("pdump: bootstrap cache miss at {}", dump_path.display());
}
let _write_lock = match BootstrapCacheWriteLock::acquire(&lock_path) {
Ok(lock) => Some(lock),
Err(BootstrapCacheLockError::Busy(err)) => {
tracing::info!("pdump: waiting for bootstrap cache writer ({err})");
match BootstrapCacheReadLock::wait(&lock_path) {
Ok(read_lock) => {
if dump_path.exists()
&& let Some(eval) =
try_load_dump(dump_path, &project_root, "after waiting for writer")?
{
return Ok(eval);
}
drop(read_lock);
match BootstrapCacheWriteLock::acquire(&lock_path) {
Ok(lock) => Some(lock),
Err(err) => {
tracing::warn!(
"pdump: cache writer handoff unavailable ({err}), bootstrapping without cache"
);
None
}
}
}
Err(wait_err) => {
tracing::warn!(
"pdump: cache wait failed ({wait_err}), bootstrapping without cache"
);
None
}
}
}
Err(BootstrapCacheLockError::Other(err)) => {
tracing::warn!("pdump: cache lock unavailable ({err}), bootstrapping without cache");
None
}
};
if _write_lock.is_none() {
let mut eval = create_bootstrap_evaluator_with_features(extra_features)?;
ensure_startup_compat_variables(&mut eval, &project_root);
finalize_or_log(
&mut eval,
&project_root,
"pdump lockless fallback finalize failed",
)?;
return Ok(eval);
}
if dump_path.exists() {
if let Some(eval) = try_load_dump(dump_path, &project_root, "after acquiring write lock")? {
return Ok(eval);
}
}
let start = std::time::Instant::now();
let mut eval = create_bootstrap_evaluator_with_features(extra_features)?;
ensure_startup_compat_variables(&mut eval, &project_root);
let bootstrap_time = start.elapsed();
if let Some(parent) = dump_path.parent()
&& !parent.exists()
{
let _ = std::fs::create_dir_all(parent);
}
let dump_start = std::time::Instant::now();
match pdump::dump_to_file(&eval, dump_path) {
Ok(()) => {
tracing::info!(
"pdump: saved bootstrap state to {} ({:.2?}, bootstrap took {:.2?})",
dump_path.display(),
dump_start.elapsed(),
bootstrap_time,
);
let reload_start = std::time::Instant::now();
match pdump::load_from_dump(dump_path) {
Ok(mut loaded) => {
finalize_or_log(
&mut loaded,
&project_root,
"pdump fresh reload finalize failed",
)?;
tracing::info!(
"pdump: reloaded freshly written bootstrap state from {} ({:.2?})",
dump_path.display(),
reload_start.elapsed()
);
return Ok(loaded);
}
Err(e) => {
tracing::warn!(
"pdump: failed to reload freshly written bootstrap image ({e}), using in-memory bootstrap"
);
}
}
}
Err(e) => {
tracing::warn!("pdump: failed to save ({e}), will bootstrap again next time");
}
}
finalize_or_log(
&mut eval,
&project_root,
"pdump in-memory fallback finalize failed",
)?;
Ok(eval)
}
pub(crate) fn expand_tilde(path: &str) -> String {
if path.starts_with("~/") {
if let Some(home) = std::env::var_os("HOME") {
return format!("{}{}", home.to_string_lossy(), &path[1..]);
}
}
path.to_string()
}
#[cfg(test)]
#[path = "load_test.rs"]
mod tests;