use super::error::{EvalResult, Flow, signal};
use super::intern::resolve_sym;
use super::value::{Value, ValueKind, VecLikeType};
#[cfg(unix)]
use std::ffi::CStr;
use std::fs;
fn expect_args(name: &str, args: &[Value], n: usize) -> Result<(), Flow> {
if args.len() != n {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_min_args(name: &str, args: &[Value], min: usize) -> Result<(), Flow> {
if args.len() < min {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_max_args(name: &str, args: &[Value], max: usize) -> Result<(), Flow> {
if args.len() > max {
Err(signal(
"wrong-number-of-arguments",
vec![Value::symbol(name), Value::fixnum(args.len() as i64)],
))
} else {
Ok(())
}
}
fn expect_string(val: &Value) -> Result<String, Flow> {
match val.kind() {
ValueKind::String => Ok(val
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload")),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *val],
)),
}
}
fn expect_int(val: &Value) -> Result<i64, Flow> {
match val.kind() {
ValueKind::Fixnum(n) => Ok(n),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), *val],
)),
}
}
fn symbol_like_name(value: &Value) -> Option<&str> {
match value.kind() {
ValueKind::Nil => Some("nil"),
ValueKind::T => Some("t"),
ValueKind::Symbol(id) => Some(resolve_sym(id)),
_ => None,
}
}
fn expect_number_or_marker_f64(value: &Value) -> Result<f64, Flow> {
use crate::emacs_core::value::VecLikeType;
match value.kind() {
ValueKind::Fixnum(n) => Ok(n as f64),
ValueKind::Float => Ok(value.xfloat()),
ValueKind::Veclike(VecLikeType::Bignum) => Ok(value.as_bignum().unwrap().to_f64()),
_ => Err(signal(
"wrong-type-argument",
vec![Value::symbol("number-or-marker-p"), *value],
)),
}
}
fn list_car_or_signal(value: &Value) -> Result<Value, Flow> {
match value.kind() {
ValueKind::Cons => Ok(value.cons_car()),
ValueKind::Nil => Ok(Value::NIL),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), *value],
)),
}
}
fn assoc_string_key_name(value: &Value) -> Result<String, Flow> {
match value.kind() {
ValueKind::String => Ok(value
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload")),
_ => symbol_like_name(value)
.map(ToOwned::to_owned)
.ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), *value],
)
}),
}
}
fn assoc_string_entry_name(value: &Value) -> Option<String> {
match value.kind() {
ValueKind::String => Some(
value
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload"),
),
_ => symbol_like_name(value).map(ToOwned::to_owned),
}
}
fn assoc_string_equal(left: &str, right: &str, fold_case: bool) -> bool {
if fold_case {
left.chars()
.flat_map(char::to_lowercase)
.eq(right.chars().flat_map(char::to_lowercase))
} else {
left == right
}
}
fn collect_sequence_strict(val: &Value) -> Result<Vec<Value>, Flow> {
match val.kind() {
ValueKind::Nil => Ok(Vec::new()),
ValueKind::Cons => {
let mut result = Vec::new();
let mut cursor = *val;
loop {
match cursor.kind() {
ValueKind::Nil => return Ok(result),
ValueKind::Cons => {
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
result.push(pair_car);
cursor = pair_cdr;
}
tail => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), cursor],
));
}
}
}
}
ValueKind::Veclike(VecLikeType::Vector) | ValueKind::Veclike(VecLikeType::Record) => {
Ok(val.as_vector_data().unwrap().clone())
}
ValueKind::String => Ok(super::builtins::lisp_string_char_codes(
val.as_lisp_string().expect("string"),
)
.into_iter()
.map(|code| Value::fixnum(code as i64))
.collect()),
other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("sequencep"), *val],
)),
}
}
pub(crate) fn remove_list_equal(args: Vec<Value>) -> EvalResult {
expect_args("remove", &args, 2)?;
let target = &args[0];
let list_val = &args[1];
let mut result = Vec::new();
let mut cursor = *list_val;
loop {
match cursor.kind() {
ValueKind::Nil => break,
ValueKind::Cons => {
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
if !super::value::equal_value(&pair_car, target, 0) {
result.push(pair_car);
}
cursor = pair_cdr;
}
_ => break,
}
}
Ok(Value::list(result))
}
pub(crate) fn builtin_take(args: Vec<Value>) -> EvalResult {
expect_args("take", &args, 2)?;
let n = expect_int(&args[0])?;
if n <= 0 {
return Ok(Value::NIL);
}
let list = &args[1];
if !list.is_nil() && !list.is_cons() {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), *list],
));
}
let mut result = Vec::new();
let mut cursor = *list;
for _ in 0..(n as usize) {
match cursor.kind() {
ValueKind::Nil => break,
ValueKind::Cons => {
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
result.push(pair_car);
cursor = pair_cdr;
}
tail => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), cursor],
));
}
}
}
Ok(Value::list(result))
}
pub(crate) fn builtin_string_search(args: Vec<Value>) -> EvalResult {
expect_min_args("string-search", &args, 2)?;
let needle_ls = args[0].as_lisp_string().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), args[0]],
)
})?;
let haystack_ls = args[1].as_lisp_string().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), args[1]],
)
})?;
let char_len = haystack_ls.schars();
let start_char = if args.len() > 2 {
let n = expect_int(&args[2])?;
if n < 0 || n as usize > char_len {
return Err(signal(
"args-out-of-range",
vec![args[2], Value::fixnum(0), Value::fixnum(char_len as i64)],
));
}
n as usize
} else {
0
};
let haystack_bytes = haystack_ls.as_bytes();
let needle_bytes = needle_ls.as_bytes();
let start_byte = if haystack_ls.is_multibyte() {
crate::emacs_core::emacs_char::char_to_byte_pos(haystack_bytes, start_char)
} else {
start_char
};
let search_in = &haystack_bytes[start_byte..];
if let Some(byte_pos) = find_subsequence(search_in, needle_bytes) {
let abs_byte = start_byte + byte_pos;
let char_pos = if haystack_ls.is_multibyte() {
crate::emacs_core::emacs_char::byte_to_char_pos(haystack_bytes, abs_byte)
} else {
abs_byte
};
Ok(Value::fixnum(char_pos as i64))
} else {
Ok(Value::NIL)
}
}
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() {
return Some(0);
}
haystack.windows(needle.len()).position(|w| w == needle)
}
pub(crate) fn builtin_proper_list_p(args: Vec<Value>) -> EvalResult {
expect_args("proper-list-p", &args, 1)?;
match super::value::list_length(&args[0]) {
Some(len) => Ok(Value::fixnum(len as i64)),
None => Ok(Value::NIL),
}
}
pub(crate) fn builtin_subrp(args: Vec<Value>) -> EvalResult {
expect_args("subrp", &args, 1)?;
Ok(Value::bool_val(args[0].as_subr_id().is_some()))
}
pub(crate) fn builtin_bare_symbol(args: Vec<Value>) -> EvalResult {
expect_args("bare-symbol", &args, 1)?;
bare_symbol_value(args[0])
}
pub(crate) fn builtin_bare_symbol_1(_eval: &mut super::eval::Context, arg: Value) -> EvalResult {
bare_symbol_value(arg)
}
fn bare_symbol_value(arg: Value) -> EvalResult {
if symbol_like_name(&arg).is_some() {
Ok(arg)
} else if arg.is_symbol_with_pos() {
Ok(arg.as_symbol_with_pos_sym().unwrap())
} else {
Err(signal(
"wrong-type-argument",
vec![Value::symbol("symbolp"), arg],
))
}
}
pub(crate) fn builtin_bare_symbol_p(args: Vec<Value>) -> EvalResult {
expect_args("bare-symbol-p", &args, 1)?;
Ok(Value::bool_val(symbol_like_name(&args[0]).is_some()))
}
pub(crate) fn builtin_byteorder(args: Vec<Value>) -> EvalResult {
expect_args("byteorder", &args, 0)?;
let marker = if cfg!(target_endian = "little") {
'l'
} else {
'B'
};
Ok(Value::fixnum(marker as i64))
}
pub(crate) fn builtin_assoc_string(args: Vec<Value>) -> EvalResult {
expect_min_args("assoc-string", &args, 2)?;
expect_max_args("assoc-string", &args, 3)?;
let needle = assoc_string_key_name(&args[0])?;
let fold_case = args.get(2).is_some_and(|v| v.is_truthy());
let mut cursor = args[1];
loop {
match cursor.kind() {
ValueKind::Nil => return Ok(Value::NIL),
ValueKind::Cons => {
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
let entry = pair_car;
cursor = pair_cdr;
if !entry.is_cons() {
continue;
};
let entry_pair_car = entry.cons_car();
let entry_pair_cdr = entry.cons_cdr();
let Some(entry_key) = assoc_string_entry_name(&entry_pair_car) else {
continue;
};
if assoc_string_equal(&needle, &entry_key, fold_case) {
return Ok(entry);
}
}
_ => return Ok(Value::NIL),
}
}
}
pub(crate) fn builtin_car_less_than_car(args: Vec<Value>) -> EvalResult {
expect_args("car-less-than-car", &args, 2)?;
let left = list_car_or_signal(&args[0])?;
let right = list_car_or_signal(&args[1])?;
Ok(Value::bool_val(
expect_number_or_marker_f64(&left)? < expect_number_or_marker_f64(&right)?,
))
}
pub(crate) fn builtin_byte_code_function_p(args: Vec<Value>) -> EvalResult {
expect_args("byte-code-function-p", &args, 1)?;
Ok(Value::bool_val(args[0].is_bytecode()))
}
pub(crate) fn builtin_compiled_function_p(args: Vec<Value>) -> EvalResult {
expect_args("compiled-function-p", &args, 1)?;
Ok(Value::bool_val(args[0].is_bytecode()))
}
pub(crate) fn builtin_closurep(args: Vec<Value>) -> EvalResult {
expect_args("closurep", &args, 1)?;
Ok(Value::bool_val(
args[0].is_lambda() || args[0].is_bytecode(),
))
}
pub(crate) fn builtin_closurep_1(_eval: &mut super::eval::Context, arg: Value) -> EvalResult {
Ok(Value::bool_val(arg.is_lambda() || arg.is_bytecode()))
}
pub(crate) fn builtin_natnump(args: Vec<Value>) -> EvalResult {
expect_args("natnump", &args, 1)?;
let is_nat = match args[0].kind() {
ValueKind::Fixnum(n) => n >= 0,
_ => false,
};
Ok(Value::bool_val(is_nat))
}
pub(crate) fn builtin_zerop(args: Vec<Value>) -> EvalResult {
expect_args("zerop", &args, 1)?;
let is_zero = match args[0].kind() {
ValueKind::Fixnum(0) => true,
ValueKind::Float => args[0].xfloat() == 0.0,
_ => false,
};
Ok(Value::bool_val(is_zero))
}
#[derive(Debug, Clone)]
struct PasswdEntry {
login: String,
uid: i64,
gecos: String,
}
fn parse_passwd_entry(line: &str) -> Option<PasswdEntry> {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
return None;
}
let mut fields = trimmed.split(':');
let login = fields.next()?.to_string();
let _passwd = fields.next()?;
let uid = fields.next()?.parse::<i64>().ok()?;
let _gid = fields.next()?;
let gecos = fields.next().unwrap_or("").to_string();
Some(PasswdEntry { login, uid, gecos })
}
fn load_passwd_entries() -> Vec<PasswdEntry> {
fs::read_to_string("/etc/passwd")
.ok()
.map(|content| content.lines().filter_map(parse_passwd_entry).collect())
.unwrap_or_default()
}
fn login_name_from_env() -> Option<String> {
std::env::var("LOGNAME")
.ok()
.or_else(|| std::env::var("USER").ok())
.filter(|name| !name.is_empty())
}
fn current_uid() -> i64 {
std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or(1000)
}
fn real_uid() -> i64 {
std::process::Command::new("id")
.args(["-ru"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.trim().parse::<i64>().ok())
.unwrap_or_else(current_uid)
}
fn lookup_login_by_uid(uid: i64) -> Option<String> {
load_passwd_entries()
.into_iter()
.find(|entry| entry.uid == uid)
.map(|entry| entry.login)
}
fn canonical_full_name(entry: &PasswdEntry) -> String {
let first_gecos = entry.gecos.split(',').next().unwrap_or("").trim();
if first_gecos.is_empty() {
entry.login.clone()
} else {
first_gecos.to_string()
}
}
fn lookup_full_name_by_uid(uid: i64) -> Option<String> {
load_passwd_entries()
.into_iter()
.find(|entry| entry.uid == uid)
.map(|entry| canonical_full_name(&entry))
}
fn lookup_full_name_by_login(login: &str) -> Option<String> {
load_passwd_entries()
.into_iter()
.find(|entry| entry.login == login)
.map(|entry| canonical_full_name(&entry))
}
fn expect_uid_arg(val: &Value) -> Result<i64, Flow> {
match val.kind() {
ValueKind::Fixnum(uid) if uid >= 0 => Ok(uid),
_ => Err(signal(
"error",
vec![Value::string(
"Not an in-range integer, integral float, or cons of integers",
)],
)),
}
}
pub(crate) fn builtin_user_login_name(args: Vec<Value>) -> EvalResult {
expect_max_args("user-login-name", &args, 1)?;
if let Some(uid_arg) = args.first() {
if uid_arg.is_nil() {
let current = login_name_from_env()
.or_else(|| lookup_login_by_uid(current_uid()))
.unwrap_or_else(|| "unknown".to_string());
return Ok(Value::string(current));
}
let uid = expect_uid_arg(uid_arg)?;
return Ok(match lookup_login_by_uid(uid) {
Some(name) => Value::string(name),
None => Value::NIL,
});
}
let current = login_name_from_env()
.or_else(|| lookup_login_by_uid(current_uid()))
.unwrap_or_else(|| "unknown".to_string());
Ok(Value::string(current))
}
pub(crate) fn builtin_user_real_login_name(args: Vec<Value>) -> EvalResult {
expect_args("user-real-login-name", &args, 0)?;
let name = lookup_login_by_uid(real_uid())
.or_else(login_name_from_env)
.unwrap_or_else(|| "unknown".to_string());
Ok(Value::string(name))
}
pub(crate) fn builtin_user_full_name(args: Vec<Value>) -> EvalResult {
expect_max_args("user-full-name", &args, 1)?;
if let Some(target) = args.first() {
if target.is_nil() {
if let Ok(name) = std::env::var("NAME") {
if !name.is_empty() {
return Ok(Value::string(name));
}
}
let fallback = lookup_full_name_by_uid(current_uid())
.or_else(|| {
login_name_from_env()
.as_deref()
.and_then(lookup_full_name_by_login)
})
.or_else(login_name_from_env)
.unwrap_or_else(|| "unknown".to_string());
return Ok(Value::string(fallback));
}
return Ok(match target.kind() {
ValueKind::Fixnum(uid) => {
if uid < 0 {
return Err(signal(
"error",
vec![Value::string(
"Not an in-range integer, integral float, or cons of integers",
)],
));
}
lookup_full_name_by_uid(uid)
.map(Value::string)
.unwrap_or(Value::NIL)
}
ValueKind::String => {
let login = target
.as_runtime_string_owned()
.expect("ValueKind::String must carry LispString payload");
lookup_full_name_by_login(&login)
.map(Value::string)
.unwrap_or(Value::NIL)
}
_ => {
return Err(signal(
"error",
vec![Value::string(
"Not an in-range integer, integral float, or cons of integers",
)],
));
}
});
}
if let Ok(name) = std::env::var("NAME") {
if !name.is_empty() {
return Ok(Value::string(name));
}
}
let fallback = lookup_full_name_by_uid(current_uid())
.or_else(|| {
login_name_from_env()
.as_deref()
.and_then(lookup_full_name_by_login)
})
.or_else(login_name_from_env)
.unwrap_or_else(|| "unknown".to_string());
Ok(Value::string(fallback))
}
pub(crate) fn builtin_system_name(args: Vec<Value>) -> EvalResult {
expect_args("system-name", &args, 0)?;
let name = hostname::get()
.map(|os| os.to_string_lossy().into_owned())
.unwrap_or_else(|_| "localhost".to_string());
let name: String = name
.chars()
.map(|c| if c == ' ' || c == '\t' { '-' } else { c })
.collect();
Ok(Value::string(name))
}
pub(crate) fn operating_system_release_value() -> Value {
operating_system_release()
.map(Value::string)
.unwrap_or(Value::NIL)
}
pub(crate) fn system_configuration_value() -> Value {
Value::string(
option_env!("TARGET")
.map(str::to_owned)
.unwrap_or_else(fallback_system_configuration),
)
}
pub(crate) fn system_configuration_options_value() -> Value {
Value::string("")
}
pub(crate) fn system_configuration_features_value() -> Value {
let mut features = vec!["PDUMPER".to_string(), "THREADS".to_string()];
features.sort_unstable();
features.dedup();
Value::string(features.join(" "))
}
fn fallback_system_configuration() -> String {
let arch = std::env::consts::ARCH;
let os = match std::env::consts::OS {
"linux" => "unknown-linux-gnu",
"macos" => "apple-darwin",
"windows" => "pc-windows-msvc",
other => other,
};
format!("{arch}-{os}")
}
fn operating_system_release() -> Option<String> {
#[cfg(unix)]
{
let mut utsname = std::mem::MaybeUninit::<libc::utsname>::uninit();
let release = unsafe {
if libc::uname(utsname.as_mut_ptr()) != 0 {
return None;
}
let utsname = utsname.assume_init();
CStr::from_ptr(utsname.release.as_ptr())
.to_string_lossy()
.trim()
.to_string()
};
if release.is_empty() {
None
} else {
Some(release)
}
}
#[cfg(not(unix))]
{
None
}
}
pub(crate) fn builtin_emacs_version(args: Vec<Value>) -> EvalResult {
expect_max_args("emacs-version", &args, 1)?;
if args.first().is_some_and(|arg| !arg.is_nil()) {
return Ok(Value::NIL);
}
Ok(Value::string(
"GNU Emacs 31.0.50 (build 1, x86_64-pc-linux-gnu) [NeoVM 0.1.0 (Neomacs)]\n Copyright (C) 2026 Free Software Foundation, Inc.",
))
}
pub(crate) fn builtin_emacs_pid(args: Vec<Value>) -> EvalResult {
expect_args("emacs-pid", &args, 0)?;
Ok(Value::fixnum(std::process::id() as i64))
}
fn gc_bucket(name: &str, counts: &[i64]) -> Value {
let mut items = Vec::with_capacity(counts.len() + 1);
items.push(Value::symbol(name));
items.extend(counts.iter().copied().map(Value::fixnum));
Value::list(items)
}
pub(crate) fn builtin_garbage_collect_stats() -> EvalResult {
let counts = Value::memory_use_counts_snapshot();
let conses = counts[0].max(0);
let floats = counts[1].max(0);
let vector_cells = counts[2].max(0);
let symbols = counts[3].max(0);
let string_chars = counts[4].max(0);
let intervals = counts[5].max(0);
let strings = counts[6].max(0);
Ok(Value::list(vec![
gc_bucket("conses", &[16, conses, 0]),
gc_bucket("symbols", &[48, symbols, 0]),
gc_bucket("strings", &[32, strings, 0]),
gc_bucket("string-bytes", &[1, string_chars]),
gc_bucket("vectors", &[16, vector_cells]),
gc_bucket("vector-slots", &[8, vector_cells, 0]),
gc_bucket("floats", &[8, floats, 0]),
gc_bucket("intervals", &[56, intervals, 0]),
gc_bucket("buffers", &[992, 0]),
]))
}
pub(crate) fn builtin_memory_use_counts(args: Vec<Value>) -> EvalResult {
expect_args("memory-use-counts", &args, 0)?;
let counts = Value::memory_use_counts_snapshot();
Ok(Value::list(
counts.iter().map(|count| Value::fixnum(*count)).collect(),
))
}
#[cfg(test)]
#[path = "builtins_extra_test.rs"]
mod tests;