use super::*;
use crate::emacs_core::value::{
ValueKind, VecLikeType, get_string_text_properties_table_for_value,
set_string_text_properties_table_for_value,
};
pub(crate) fn builtin_string_equal(args: Vec<Value>) -> EvalResult {
expect_args("string-equal", &args, 2)?;
builtin_string_equal_values(args[0], args[1])
}
pub(crate) fn builtin_string_equal_2(
_eval: &mut super::eval::Context,
a_value: Value,
b_value: Value,
) -> EvalResult {
builtin_string_equal_values(a_value, b_value)
}
fn builtin_string_equal_values(a_value: Value, b_value: Value) -> EvalResult {
let a = expect_string_comparison_operand(&a_value)?;
let b = expect_string_comparison_operand(&b_value)?;
Ok(Value::bool_val(a == b))
}
pub(crate) fn builtin_string_lessp(args: Vec<Value>) -> EvalResult {
expect_args("string-lessp", &args, 2)?;
builtin_string_lessp_values(args[0], args[1])
}
pub(crate) fn builtin_string_lessp_2(
_eval: &mut super::eval::Context,
a_value: Value,
b_value: Value,
) -> EvalResult {
builtin_string_lessp_values(a_value, b_value)
}
fn builtin_string_lessp_values(a_value: Value, b_value: Value) -> EvalResult {
let a = expect_string_comparison_operand(&a_value)?;
let b = expect_string_comparison_operand(&b_value)?;
Ok(Value::bool_val(a < b))
}
pub(crate) fn builtin_string_greaterp(args: Vec<Value>) -> EvalResult {
expect_args("string-greaterp", &args, 2)?;
let a = expect_string_comparison_operand(&args[0])?;
let b = expect_string_comparison_operand(&args[1])?;
Ok(Value::bool_val(a > b))
}
fn substring_impl(name: &str, args: &[Value], preserve_props: bool) -> EvalResult {
expect_min_args(name, args, 1)?;
expect_max_args(name, args, 3)?;
match args[0].kind() {
ValueKind::String => {
let src_props = if preserve_props {
get_string_text_properties_table_for_value(args[0])
.filter(|table| !table.is_empty())
} else {
None
};
let src = args[0].as_lisp_string().unwrap();
let (result, sliced_props) = (|| {
let src_bytes = src.as_bytes();
let normalize_index =
|value: &Value, default: i64, len: i64| -> Result<i64, Flow> {
let raw = if value.is_nil() {
default
} else {
expect_int(value)?
};
let idx = if raw < 0 { len + raw } else { raw };
if idx < 0 || idx > len {
return Err(signal(
"args-out-of-range",
vec![args[0], args[1], args.get(2).cloned().unwrap_or(Value::NIL)],
));
}
Ok(idx)
};
if src_props.is_none() && !src.is_multibyte() {
let len = src_bytes.len() as i64;
let from = if args.len() > 1 {
normalize_index(&args[1], 0, len)?
} else {
0
} as usize;
let to = if args.len() > 2 {
normalize_index(&args[2], len, len)?
} else {
len
} as usize;
if from > to {
return Err(signal(
"args-out-of-range",
vec![
args[0],
args.get(1).cloned().unwrap_or(Value::fixnum(0)),
args.get(2).cloned().unwrap_or(Value::NIL),
],
));
}
return Ok::<_, Flow>((
src.slice(from, to).expect("validated ascii slice"),
None,
));
}
let len = src.schars() as i64;
let from = if args.len() > 1 {
normalize_index(&args[1], 0, len)?
} else {
0
} as usize;
let to = if args.len() > 2 {
normalize_index(&args[2], len, len)?
} else {
len
} as usize;
if from > to {
return Err(signal(
"args-out-of-range",
vec![
args[0],
args.get(1).cloned().unwrap_or(Value::fixnum(0)),
args.get(2).cloned().unwrap_or(Value::NIL),
],
));
}
let (byte_from, byte_to) = if src.is_multibyte() {
let bf = crate::emacs_core::emacs_char::char_to_byte_pos(src_bytes, from);
let bt = crate::emacs_core::emacs_char::char_to_byte_pos(src_bytes, to);
(bf, bt)
} else {
(from, to)
};
if byte_to > src_bytes.len() {
return Err(signal(
"args-out-of-range",
vec![
args[0],
args.get(1).cloned().unwrap_or(Value::fixnum(0)),
args.get(2).cloned().unwrap_or(Value::NIL),
],
));
}
let result = src
.slice(byte_from, byte_to)
.expect("validated storage substring bounds");
let sliced_props = if let Some(src_table) = src_props.as_ref() {
let sliced = src_table.slice(byte_from, byte_to);
(!sliced.is_empty()).then_some(sliced)
} else {
None
};
Ok::<_, Flow>((result, sliced_props))
})()?;
let new_val = Value::heap_string(result);
if preserve_props && new_val.is_string() {
if let Some(sliced) = sliced_props {
set_string_text_properties_table_for_value(new_val, sliced);
}
}
Ok(new_val)
}
ValueKind::Veclike(VecLikeType::Vector) | ValueKind::Veclike(VecLikeType::Record)
if name == "substring" =>
{
let items = args[0].as_vector_data().unwrap().clone();
let len = items.len() as i64;
let normalize_index = |value: &Value, default: i64| -> Result<i64, Flow> {
let raw = if value.is_nil() {
default
} else {
expect_int(value)?
};
let idx = if raw < 0 { len + raw } else { raw };
if idx < 0 || idx > len {
return Err(signal(
"args-out-of-range",
vec![args[0], args[1], args.get(2).cloned().unwrap_or(Value::NIL)],
));
}
Ok(idx)
};
let from = if args.len() > 1 {
normalize_index(&args[1], 0)?
} else {
0
} as usize;
let to = if args.len() > 2 {
normalize_index(&args[2], len)?
} else {
len
} as usize;
if from > to {
return Err(signal(
"args-out-of-range",
vec![
args[0],
args.get(1).cloned().unwrap_or(Value::fixnum(0)),
args.get(2).cloned().unwrap_or(Value::NIL),
],
));
}
Ok(Value::vector(items[from..to].to_vec()))
}
_ => {
let s = expect_string(&args[0])?;
let _ = s;
unreachable!("expect_string either returns a string or signals")
}
}
}
#[cfg(test)]
#[path = "strings_test.rs"]
mod tests;
pub(crate) fn builtin_substring(args: Vec<Value>) -> EvalResult {
builtin_substring_slice(&args)
}
pub(crate) fn builtin_substring_slice(args: &[Value]) -> EvalResult {
crate::emacs_core::perf_trace::time_op(
crate::emacs_core::perf_trace::HotpathOp::Substring,
|| substring_impl("substring", args, true),
)
}
pub(crate) fn builtin_substring_no_properties(args: Vec<Value>) -> EvalResult {
crate::emacs_core::perf_trace::time_op(
crate::emacs_core::perf_trace::HotpathOp::Substring,
|| substring_impl("substring-no-properties", &args, false),
)
}
pub(crate) fn builtin_concat(args: Vec<Value>) -> EvalResult {
builtin_concat_slice(&args)
}
pub(crate) fn builtin_concat_slice(args: &[Value]) -> EvalResult {
crate::emacs_core::perf_trace::time_op(crate::emacs_core::perf_trace::HotpathOp::Concat, || {
use crate::emacs_core::emacs_char;
fn push_concat_int(result: &mut Vec<u8>, n: i64) -> Result<(), Flow> {
if !(0..=0x3FFFFF).contains(&n) {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), Value::fixnum(n)],
));
}
let cp = n as u32;
let mut buf = [0u8; emacs_char::MAX_MULTIBYTE_LENGTH];
let len = emacs_char::char_string(cp, &mut buf);
result.extend_from_slice(&buf[..len]);
Ok(())
}
fn push_concat_element(result: &mut Vec<u8>, value: &Value) -> Result<(), Flow> {
match value.kind() {
ValueKind::Fixnum(c) => push_concat_int(result, c),
_ => Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), *value],
)),
}
}
if args.iter().all(|arg| arg.is_string()) {
let has_text_props = args.iter().any(|arg| {
get_string_text_properties_table_for_value(*arg)
.is_some_and(|table| !table.is_empty())
});
if !has_text_props {
let mut combined = Vec::new();
let mut multibyte = false;
for arg in args {
if let Some(string) = arg.as_lisp_string() {
combined.extend_from_slice(string.as_bytes());
multibyte |= string.is_multibyte();
}
}
let result = if multibyte {
crate::heap_types::LispString::from_emacs_bytes(combined)
} else {
crate::heap_types::LispString::from_unibyte(combined)
};
return Ok(Value::heap_string(result));
}
}
let preallocated_len = args.iter().fold(0usize, |acc, arg| match arg.kind() {
ValueKind::String => acc + arg.as_lisp_string().map(|ls| ls.sbytes()).unwrap_or(0),
_ => acc,
});
let mut result: Vec<u8> = Vec::with_capacity(preallocated_len);
let mut string_sources: Vec<(Value, usize)> = Vec::new();
for arg in args {
match arg.kind() {
ValueKind::String => {
let offset = result.len();
if let Some(ls) = arg.as_lisp_string() {
result.extend_from_slice(ls.as_bytes());
}
string_sources.push((*arg, offset));
}
ValueKind::Nil => {}
ValueKind::Cons => {
let mut cursor = *arg;
loop {
match cursor.kind() {
ValueKind::Nil => break,
ValueKind::Cons => {
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
push_concat_element(&mut result, &pair_car)?;
cursor = pair_cdr;
}
_tail => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("listp"), cursor],
));
}
}
}
}
ValueKind::Veclike(VecLikeType::Vector) => {
let items = arg.as_vector_data().unwrap().clone();
for item in items.iter() {
push_concat_element(&mut result, item)?;
}
}
_ => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("sequencep"), *arg],
));
}
}
}
let new_val = Value::heap_string(crate::heap_types::LispString::from_emacs_bytes(result));
if new_val.is_string() {
let mut combined_table = crate::buffer::text_props::TextPropertyTable::new();
let mut has_props = false;
for (src_val, offset) in &string_sources {
if let Some(src_table) = get_string_text_properties_table_for_value(*src_val) {
if !src_table.is_empty() {
combined_table.append_shifted(&src_table, *offset);
has_props = true;
}
}
}
if has_props {
set_string_text_properties_table_for_value(new_val, combined_table);
}
}
Ok(new_val)
})
}
pub(crate) fn builtin_string_to_number(args: Vec<Value>) -> EvalResult {
expect_min_args("string-to-number", &args, 1)?;
expect_max_args("string-to-number", &args, 2)?;
let s = expect_string(&args[0])?;
let base = if args.len() > 1 {
expect_int(&args[1])?
} else {
10
};
if !(2..=16).contains(&base) {
return Err(signal("args-out-of-range", vec![Value::fixnum(base)]));
}
let s = s.trim_start_matches(|c: char| c == ' ' || c == '\t');
if base == 10 {
let number_prefix =
Regex::new(r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?")
.expect("number prefix regexp should compile");
if let Some(m) = number_prefix.find(s) {
let token = m.as_str();
let has_trail_int = if let Some(dot_pos) = token.find('.') {
token[dot_pos + 1..]
.chars()
.next()
.map_or(false, |c| c.is_ascii_digit())
} else {
false
};
let has_e_exp = token.contains('e') || token.contains('E');
let has_lead_int = token
.trim_start_matches(['+', '-'])
.starts_with(|c: char| c.is_ascii_digit());
let is_float = has_trail_int || (has_lead_int && has_e_exp);
if is_float {
if let Ok(f) = token.parse::<f64>() {
return Ok(Value::make_float(f));
}
} else {
let int_token = if let Some(dot_pos) = token.find('.') {
&token[..dot_pos]
} else {
token
};
if let Ok(n) = int_token.parse::<i64>() {
return Ok(Value::make_integer(rug::Integer::from(n)));
}
if let Ok(parsed) = rug::Integer::parse(int_token) {
return Ok(Value::make_integer(rug::Integer::from(parsed)));
}
}
}
} else {
let bytes = s.as_bytes();
let mut pos = 0usize;
let mut negative = false;
if pos < bytes.len() {
if bytes[pos] == b'+' {
pos += 1;
} else if bytes[pos] == b'-' {
negative = true;
pos += 1;
}
}
let digit_start = pos;
while pos < bytes.len() {
let ch = bytes[pos] as char;
let Some(d) = ch.to_digit(36) else { break };
if (d as i64) < base {
pos += 1;
} else {
break;
}
}
if pos > digit_start {
let token = &s[digit_start..pos];
if let Ok(parsed) = i64::from_str_radix(token, base as u32) {
return Ok(Value::make_integer(rug::Integer::from(if negative {
-parsed
} else {
parsed
})));
}
let mut signed = String::with_capacity(token.len() + 1);
if negative {
signed.push('-');
}
signed.push_str(token);
if let Ok(parsed) = rug::Integer::parse_radix(&signed, base as i32) {
return Ok(Value::make_integer(rug::Integer::from(parsed)));
}
}
}
Ok(Value::fixnum(0))
}
pub(crate) fn builtin_number_to_string(args: Vec<Value>) -> EvalResult {
expect_args("number-to-string", &args, 1)?;
match args[0].kind() {
ValueKind::Fixnum(n) => Ok(Value::string(n.to_string())),
ValueKind::Float => Ok(Value::string(super::print::format_float(args[0].xfloat()))),
ValueKind::Veclike(VecLikeType::Bignum) => {
Ok(Value::string(args[0].as_bignum().unwrap().to_string()))
}
_other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("numberp"), args[0]],
)),
}
}
pub(crate) fn builtin_upcase(args: Vec<Value>) -> EvalResult {
expect_args("upcase", &args, 1)?;
match args[0].kind() {
ValueKind::String => {
let string = args[0].as_lisp_string().expect("string");
let rendered = super::runtime_string_from_lisp_string(string);
let upcased = upcase_string_emacs_compat(&rendered);
Ok(Value::heap_string(super::runtime_string_to_lisp_string(
&upcased,
runtime_string_result_multibyte(string.is_multibyte(), &upcased),
)))
}
ValueKind::Fixnum(c) if (0..=0x3F_FFFF).contains(&c) => {
let mapped = upcase_char_code_emacs_compat(c as i64);
if let Some(ch) = u32::try_from(mapped).ok().and_then(char::from_u32) {
Ok(Value::fixnum(ch as i64))
} else {
Ok(Value::fixnum(c))
}
}
ValueKind::Fixnum(_) => Err(signal(
"wrong-type-argument",
vec![Value::symbol("char-or-string-p"), args[0]],
)),
_other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("char-or-string-p"), args[0]],
)),
}
}
fn preserve_emacs_upcase_payload(code: i64) -> bool {
matches!(
code,
305
| 329
| 383
| 411
| 496
| 612
| 912
| 944
| 1415
| 7306
| 7830..=7834
| 8016
| 8018
| 8020
| 8022
| 8072..=8079
| 8088..=8095
| 8104..=8111
| 8114
| 8116
| 8118..=8119
| 8124
| 8130
| 8132
| 8134..=8135
| 8140
| 8146..=8147
| 8150..=8151
| 8162..=8164
| 8166..=8167
| 8178
| 8180
| 8182..=8183
| 8188
| 42957
| 42959
| 42963
| 42965
| 42971
| 64256..=64262
| 64275..=64279
| 68976..=68997
| 93883..=93907
)
}
fn upcase_string_emacs_compat(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
let code = ch as i64;
if ch == '\u{0131}' || preserve_emacs_upcase_string_payload(code) {
out.push(ch);
continue;
}
for up in ch.to_uppercase() {
out.push(up);
}
}
out
}
fn upcase_char_code_emacs_compat(code: i64) -> i64 {
if preserve_emacs_upcase_payload(code) {
return code;
}
match code {
223 => 7838,
8064..=8071 | 8080..=8087 | 8096..=8103 => code + 8,
8115 | 8131 | 8179 => code + 9,
_ => {
if let Some(c) = u32::try_from(code).ok().and_then(char::from_u32) {
c.to_uppercase().next().unwrap_or(c) as i64
} else {
code
}
}
}
}
fn preserve_emacs_upcase_string_payload(code: i64) -> bool {
matches!(
code,
411
| 612
| 7306
| 42957
| 42959
| 42963
| 42965
| 42971
| 68976..=68997
| 93883..=93907
)
}
fn runtime_string_result_multibyte(source_is_multibyte: bool, rendered: &str) -> bool {
source_is_multibyte
|| super::super::string_escape::decode_storage_char_codes(rendered)
.into_iter()
.any(|code| code > 0xFF)
}
fn preserve_emacs_downcase_payload(code: i64) -> bool {
matches!(
code,
304
| 7305
| 8490
| 42955
| 42956
| 42958
| 42962
| 42964
| 42970
| 42972
| 68944..=68965
| 93856..=93880
)
}
pub(crate) fn downcase_char_code_emacs_compat(code: i64) -> i64 {
if preserve_emacs_downcase_payload(code) {
return code;
}
if let Some(c) = u32::try_from(code).ok().and_then(char::from_u32) {
c.to_lowercase().next().unwrap_or(c) as i64
} else {
code
}
}
pub(crate) fn builtin_downcase(args: Vec<Value>) -> EvalResult {
expect_args("downcase", &args, 1)?;
match args[0].kind() {
ValueKind::String => {
let string = args[0].as_lisp_string().expect("string");
let rendered = super::runtime_string_from_lisp_string(string);
let downcased = downcase_string_emacs_compat(&rendered);
Ok(Value::heap_string(super::runtime_string_to_lisp_string(
&downcased,
runtime_string_result_multibyte(string.is_multibyte(), &downcased),
)))
}
ValueKind::Fixnum(c) if (0..=0x3F_FFFF).contains(&c) => {
let mapped = downcase_char_code_emacs_compat(c as i64);
if let Some(ch) = u32::try_from(mapped).ok().and_then(char::from_u32) {
Ok(Value::fixnum(ch as i64))
} else {
Ok(Value::fixnum(c))
}
}
ValueKind::Fixnum(_) => Err(signal(
"wrong-type-argument",
vec![Value::symbol("char-or-string-p"), args[0]],
)),
_other => Err(signal(
"wrong-type-argument",
vec![Value::symbol("char-or-string-p"), args[0]],
)),
}
}
fn downcase_string_emacs_compat(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
let code = ch as i64;
if ch == '\u{212A}' || preserve_emacs_downcase_string_payload(code) {
out.push(ch);
continue;
}
for low in ch.to_lowercase() {
out.push(low);
}
}
out
}
fn preserve_emacs_downcase_string_payload(code: i64) -> bool {
matches!(
code,
7305
| 42955
| 42956
| 42958
| 42962
| 42964
| 42970
| 42972
| 68944..=68965
| 93856..=93880
)
}
pub(crate) fn builtin_ngettext(args: Vec<Value>) -> EvalResult {
expect_args("ngettext", &args, 3)?;
let singular = expect_strict_string(&args[0])?;
let plural = expect_strict_string(&args[1])?;
let count = expect_int(&args[2])?;
if count == 1 {
Ok(Value::string(singular))
} else {
Ok(Value::string(plural))
}
}
pub(crate) fn builtin_format(eval: &mut super::eval::Context, args: Vec<Value>) -> EvalResult {
builtin_format_slice(eval, &args)
}
pub(crate) fn builtin_format_slice(eval: &mut super::eval::Context, args: &[Value]) -> EvalResult {
builtin_format_wrapper_strict_slice(eval, args)
}
fn format_percent_s_in_state(ctx: &crate::emacs_core::eval::Context, value: &Value) -> String {
super::misc_eval::print_value_princ_in_state(ctx, value)
}
fn format_not_enough_args_error() -> Flow {
signal(
"error",
vec![Value::string("Not enough arguments for format string")],
)
}
fn format_spec_type_mismatch_error() -> Flow {
signal(
"error",
vec![Value::string(
"Format specifier doesn’t match argument type",
)],
)
}
fn format_char_argument(n: i64) -> Result<String, Flow> {
if !(0..=KEY_CHAR_CODE_MASK).contains(&n) {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), Value::fixnum(n)],
));
}
write_char_rendered_text(n).ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), Value::fixnum(n)],
)
})
}
struct FormatSpec {
field_number: Option<usize>,
minus: bool,
plus: bool,
space: bool,
zero: bool,
sharp: bool,
width: Option<usize>,
precision: Option<usize>,
conversion: char,
}
fn parse_format_spec(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<FormatSpec> {
let mut spec = FormatSpec {
field_number: None,
minus: false,
plus: false,
space: false,
zero: false,
sharp: false,
width: None,
precision: None,
conversion: '\0',
};
let mut lookahead = chars.clone();
let mut field_digits = String::new();
while let Some(ch) = lookahead.peek().copied() {
if ch.is_ascii_digit() {
field_digits.push(ch);
lookahead.next();
} else {
break;
}
}
if !field_digits.is_empty() && lookahead.peek() == Some(&'$') {
for _ in 0..field_digits.len() {
chars.next();
}
chars.next();
spec.field_number = field_digits.parse().ok();
}
loop {
match chars.peek() {
Some('-') => {
spec.minus = true;
chars.next();
}
Some('+') => {
spec.plus = true;
chars.next();
}
Some(' ') => {
spec.space = true;
chars.next();
}
Some('0') => {
spec.zero = true;
chars.next();
}
Some('#') => {
spec.sharp = true;
chars.next();
}
_ => break,
}
}
if spec.plus {
spec.space = false;
}
if spec.minus {
spec.zero = false;
}
let mut width_str = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
width_str.push(ch);
chars.next();
} else {
break;
}
}
if !width_str.is_empty() {
spec.width = width_str.parse().ok();
}
if chars.peek() == Some(&'.') {
chars.next();
let mut prec_str = String::new();
while let Some(&ch) = chars.peek() {
if ch.is_ascii_digit() {
prec_str.push(ch);
chars.next();
} else {
break;
}
}
spec.precision = Some(if prec_str.is_empty() {
0
} else {
prec_str.parse().unwrap_or(0)
});
}
spec.conversion = chars.next()?;
Some(spec)
}
fn apply_width(s: &str, spec: &FormatSpec) -> String {
let w = match spec.width {
Some(w) if w > s.chars().count() => w,
_ => return s.to_string(),
};
let pad_char = if spec.zero && !spec.minus { '0' } else { ' ' };
if spec.minus {
format!("{:<width$}", s, width = w)
} else if spec.zero && !spec.minus {
if s.starts_with('-') {
format!("-{:0>width$}", &s[1..], width = w - 1)
} else if s.starts_with('+') {
format!("+{:0>width$}", &s[1..], width = w - 1)
} else {
format!("{:0>width$}", s, width = w)
}
} else {
format!("{:>width$}", s, width = w)
}
}
fn format_int_spec(n: i64, spec: &FormatSpec) -> String {
let s = match spec.conversion {
'd' => {
if spec.plus && n >= 0 {
format!("+{}", n)
} else if spec.space && n >= 0 {
format!(" {}", n)
} else {
n.to_string()
}
}
'o' => {
let negative = n < 0;
let abs_val = (n as i128).unsigned_abs() as u64;
let sign = if negative { "-" } else { "" };
let prefix = if spec.sharp && abs_val != 0 { "0" } else { "" };
format!("{}{}{:o}", sign, prefix, abs_val)
}
'b' | 'B' => {
let negative = n < 0;
let abs_val = (n as i128).unsigned_abs() as u64;
let sign = if negative { "-" } else { "" };
let prefix = if spec.sharp && abs_val != 0 {
if spec.conversion == 'B' { "0B" } else { "0b" }
} else {
""
};
format!("{}{}{:b}", sign, prefix, abs_val)
}
'x' => {
let negative = n < 0;
let abs_val = (n as i128).unsigned_abs() as u64;
let sign = if negative { "-" } else { "" };
let prefix = if spec.sharp && abs_val != 0 { "0x" } else { "" };
format!("{}{}{:x}", sign, prefix, abs_val)
}
'X' => {
let negative = n < 0;
let abs_val = (n as i128).unsigned_abs() as u64;
let sign = if negative { "-" } else { "" };
let prefix = if spec.sharp && abs_val != 0 { "0X" } else { "" };
format!("{}{}{:X}", sign, prefix, abs_val)
}
'i' => {
if spec.plus && n >= 0 {
format!("+{}", n)
} else if spec.space && n >= 0 {
format!(" {}", n)
} else {
n.to_string()
}
}
_ => n.to_string(),
};
apply_width(&s, spec)
}
fn format_bignum_spec(n: &rug::Integer, spec: &FormatSpec) -> String {
let negative = n.cmp0().is_lt();
let s = match spec.conversion {
'd' => {
let body = n.to_string();
if !negative && spec.plus {
format!("+{}", body)
} else if !negative && spec.space {
format!(" {}", body)
} else {
body
}
}
'b' | 'B' | 'o' | 'x' | 'X' => {
let radix: i32 = match spec.conversion {
'b' | 'B' => 2,
'o' => 8,
'x' | 'X' => 16,
_ => unreachable!(),
};
let abs = rug::Integer::from(n.abs_ref());
let mut digits = abs.to_string_radix(radix);
if spec.conversion == 'X' {
digits.make_ascii_uppercase();
}
let prefix = if spec.sharp && !abs.is_zero() {
match spec.conversion {
'b' => "0b",
'B' => "0B",
'o' => "0",
'x' => "0x",
'X' => "0X",
_ => "",
}
} else {
""
};
let sign = if negative { "-" } else { "" };
format!("{}{}{}", sign, prefix, digits)
}
'i' => {
let body = n.to_string();
if !negative && spec.plus {
format!("+{}", body)
} else if !negative && spec.space {
format!(" {}", body)
} else {
body
}
}
_ => n.to_string(),
};
apply_width(&s, spec)
}
fn normalize_exp_notation(s: &str) -> String {
if let Some(e_pos) = s.rfind('e').or_else(|| s.rfind('E')) {
let (mantissa, exp_part) = s.split_at(e_pos);
let e_char = &exp_part[..1];
let rest = &exp_part[1..];
let (sign, digits) = if rest.starts_with('+') || rest.starts_with('-') {
(&rest[..1], &rest[1..])
} else {
("+", rest)
};
let padded = if digits.len() < 2 {
format!("{:0>2}", digits)
} else {
digits.to_string()
};
format!("{}{}{}{}", mantissa, e_char, sign, padded)
} else {
s.to_string()
}
}
fn format_float_spec(f: f64, spec: &FormatSpec) -> String {
let prec = spec.precision.unwrap_or(6);
let s = match spec.conversion {
'f' => format!("{:.prec$}", f, prec = prec),
'e' => normalize_exp_notation(&format!("{:.prec$e}", f, prec = prec)),
'E' => normalize_exp_notation(&format!("{:.prec$E}", f, prec = prec)),
'g' | 'G' => {
let p = if prec == 0 { 1 } else { prec };
let exp_fmt = format!("{:.prec$e}", f, prec = p.saturating_sub(1));
let exp_val = exp_fmt
.rfind('e')
.and_then(|i| exp_fmt[i + 1..].parse::<i32>().ok())
.unwrap_or(0);
if exp_val < -4 || exp_val >= p as i32 {
let mut s = format!("{:.prec$e}", f, prec = p.saturating_sub(1));
if let Some(e_pos) = s.rfind('e') {
let mantissa = &s[..e_pos];
let exp_part = &s[e_pos..];
let trimmed = mantissa.trim_end_matches('0');
let trimmed = trimmed.trim_end_matches('.');
s = format!("{}{}", trimmed, exp_part);
}
s = normalize_exp_notation(&s);
if spec.conversion == 'G' {
s = s.replace('e', "E");
}
s
} else {
let decimal_places = if exp_val >= 0 {
p.saturating_sub(exp_val as usize + 1)
} else {
p
};
let mut s = format!("{:.prec$}", f, prec = decimal_places);
if s.contains('.') {
s = s.trim_end_matches('0').to_string();
s = s.trim_end_matches('.').to_string();
}
s
}
}
_ => format!("{:.prec$}", f, prec = prec),
};
let s = if spec.plus && f >= 0.0 && !f.is_nan() {
format!("+{}", s)
} else if spec.space && f >= 0.0 && !f.is_nan() {
format!(" {}", s)
} else {
s
};
apply_width(&s, spec)
}
fn format_string_spec(s: &str, spec: &FormatSpec) -> String {
format_string_spec_tracked(s, spec).0
}
fn format_string_spec_tracked(s: &str, spec: &FormatSpec) -> (String, usize, usize) {
let truncated = if let Some(prec) = spec.precision {
if prec < s.chars().count() {
&s[..s.char_indices().nth(prec).map_or(s.len(), |(i, _)| i)]
} else {
s
}
} else {
s
};
let content_bytes = truncated.len();
let content_chars = truncated.chars().count();
let w = match spec.width {
Some(w) if w > content_chars => w,
_ => return (truncated.to_string(), 0, content_bytes),
};
let pad_chars = w - content_chars;
if spec.minus {
let padded = format!("{:<width$}", truncated, width = w);
(padded, 0, content_bytes)
} else if spec.zero && !spec.minus {
let padded = if truncated.starts_with('-') {
format!("-{:0>width$}", &truncated[1..], width = w - 1)
} else if truncated.starts_with('+') {
format!("+{:0>width$}", &truncated[1..], width = w - 1)
} else {
format!("{:0>width$}", truncated, width = w)
};
let start = padded.len() - content_bytes;
(padded, start, start + content_bytes)
} else {
let padded = format!("{:>width$}", truncated, width = w);
(padded, pad_chars, pad_chars + content_bytes)
}
}
fn format_value_princ(val: &Value) -> String {
super::misc_eval::print_value_princ(val)
}
#[derive(Debug)]
pub(crate) struct FormatPropSpan {
pub result_byte_start: usize,
pub result_byte_end: usize,
pub arg_idx: usize,
pub arg_byte_start: usize,
pub arg_byte_end: usize,
}
fn do_format(
args: &[Value],
princ_fn: &dyn Fn(&Value) -> String,
prin1_fn: &dyn Fn(&Value) -> String,
) -> Result<(String, Vec<FormatPropSpan>), Flow> {
let fmt_str = expect_strict_string(&args[0])?;
let mut result = String::new();
let mut spans: Vec<FormatPropSpan> = Vec::new();
let mut arg_idx = 1;
let mut chars = fmt_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch != '%' {
result.push(ch);
continue;
}
let Some(spec) = parse_format_spec(&mut chars) else {
result.push('%');
continue;
};
if spec.conversion == '%' {
result.push('%');
continue;
}
let this_arg_idx = spec.field_number.unwrap_or(arg_idx);
if this_arg_idx >= args.len() {
return Err(format_not_enough_args_error());
}
let formatted = match spec.conversion {
's' => {
let s = princ_fn(&args[this_arg_idx]);
let arg_is_string = args[this_arg_idx].is_string();
let (formatted, content_byte_start_in_formatted, content_byte_end_in_formatted) =
format_string_spec_tracked(&s, &spec);
if arg_is_string && content_byte_start_in_formatted < content_byte_end_in_formatted
{
let result_byte_start = result.len() + content_byte_start_in_formatted;
let result_byte_end = result.len() + content_byte_end_in_formatted;
let arg_bytes_in =
content_byte_end_in_formatted - content_byte_start_in_formatted;
spans.push(FormatPropSpan {
result_byte_start,
result_byte_end,
arg_idx: this_arg_idx,
arg_byte_start: 0,
arg_byte_end: arg_bytes_in,
});
}
formatted
}
'S' => {
let s = prin1_fn(&args[this_arg_idx]);
format_string_spec(&s, &spec)
}
'd' | 'i' | 'b' | 'B' | 'o' | 'x' | 'X' => {
let formatted = match args[this_arg_idx].kind() {
ValueKind::Fixnum(i) => format_int_spec(i, &spec),
ValueKind::Float => {
let f = args[this_arg_idx].xfloat();
if !f.is_finite() {
return Err(format_spec_type_mismatch_error());
}
let big = rug::Integer::from_f64(f)
.ok_or_else(format_spec_type_mismatch_error)?;
format_bignum_spec(&big, &spec)
}
ValueKind::Veclike(VecLikeType::Bignum) => {
format_bignum_spec(args[this_arg_idx].as_bignum().unwrap(), &spec)
}
_ => {
return Err(format_spec_type_mismatch_error());
}
};
formatted
}
'f' | 'e' | 'E' | 'g' | 'G' => {
let f = expect_number(&args[this_arg_idx])
.map_err(|_| format_spec_type_mismatch_error())?;
format_float_spec(f, &spec)
}
'c' => {
let n = expect_int(&args[this_arg_idx])
.map_err(|_| format_spec_type_mismatch_error())?;
let s = format_char_argument(n)?;
format_string_spec(&s, &spec)
}
_ => {
return Err(signal(
"error",
vec![Value::string(format!(
"Invalid format operation %{}",
spec.conversion
))],
));
}
};
arg_idx = this_arg_idx + 1;
result.push_str(&formatted);
}
Ok((result, spans))
}
pub(crate) fn builtin_format_wrapper_strict(
ctx: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_format_wrapper_strict_slice(ctx, &args)
}
pub(crate) fn builtin_format_wrapper_strict_slice(
ctx: &mut super::eval::Context,
args: &[Value],
) -> EvalResult {
crate::emacs_core::perf_trace::time_op(crate::emacs_core::perf_trace::HotpathOp::Format, || {
expect_min_args("format", args, 1)?;
let (s, spans) = do_format(args, &|v| format_percent_s_in_state(ctx, v), &|v| {
super::error::print_value_in_state(ctx, v)
})?;
let multibyte = args.iter().any(|value| value.string_is_multibyte())
|| runtime_string_result_multibyte(false, &s);
let result = Value::heap_string(super::runtime_string_to_lisp_string(&s, multibyte));
apply_format_prop_spans(result, &args, &spans);
Ok(result)
})
}
fn apply_format_prop_spans(result: Value, args: &[Value], spans: &[FormatPropSpan]) {
if spans.is_empty() {
return;
}
let mut table = crate::emacs_core::value::get_string_text_properties_table_for_value(result)
.unwrap_or_else(crate::buffer::text_props::TextPropertyTable::new);
let mut touched = false;
for span in spans {
let Some(arg) = args.get(span.arg_idx) else {
continue;
};
let Some(src_table) =
crate::emacs_core::value::get_string_text_properties_table_for_value(*arg)
else {
continue;
};
let sliced = src_table.slice(span.arg_byte_start, span.arg_byte_end);
if sliced.intervals_snapshot().iter().next().is_none() {
continue;
}
table.append_shifted(&sliced, span.result_byte_start);
touched = true;
}
if touched {
crate::emacs_core::value::set_string_text_properties_table_for_value(result, table);
}
}
fn apply_text_quoting_bytes(bytes: &[u8]) -> (Vec<u8>, bool) {
let mut out = Vec::with_capacity(bytes.len());
let mut substituted = false;
for &b in bytes {
match b {
b'`' => {
out.extend_from_slice(&[0xE2, 0x80, 0x98]);
substituted = true;
}
b'\'' => {
out.extend_from_slice(&[0xE2, 0x80, 0x99]);
substituted = true;
}
_ => out.push(b),
}
}
(out, substituted)
}
pub(crate) fn builtin_format_message(
ctx: &mut super::eval::Context,
args: Vec<Value>,
) -> EvalResult {
builtin_format_message_slice(ctx, &args)
}
pub(crate) fn builtin_format_message_slice(
ctx: &mut super::eval::Context,
args: &[Value],
) -> EvalResult {
expect_min_args("format-message", args, 1)?;
let formatted = builtin_format_wrapper_strict_slice(ctx, args)?;
match formatted.kind() {
ValueKind::String => {
let string = formatted.as_lisp_string().expect("string");
let (quoted_bytes, substituted) = apply_text_quoting_bytes(string.as_bytes());
let result = if string.is_multibyte() || substituted {
crate::heap_types::LispString::from_emacs_bytes(quoted_bytes)
} else {
crate::heap_types::LispString::from_unibyte(quoted_bytes)
};
Ok(Value::heap_string(result))
}
_other => Ok(formatted),
}
}
pub(crate) fn builtin_make_string(args: Vec<Value>) -> EvalResult {
expect_min_args("make-string", &args, 2)?;
expect_max_args("make-string", &args, 3)?;
let count_raw = expect_int(&args[0])?;
if count_raw < 0 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("wholenump"), args[0]],
));
}
let count = count_raw as usize;
let ch = match args[1].kind() {
ValueKind::Fixnum(c) => {
if c < 0 {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[1]],
));
}
c as u32
}
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[1]],
));
}
};
let multibyte = args.get(2).is_some_and(|v| v.is_truthy()) || ch > 0x7f;
use crate::emacs_core::emacs_char;
if ch > emacs_char::MAX_CHAR {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[1]],
));
}
if multibyte {
let mut buf = [0u8; emacs_char::MAX_MULTIBYTE_LENGTH];
let len = emacs_char::char_string(ch, &mut buf);
let unit = &buf[..len];
let mut data = Vec::with_capacity(len * count);
for _ in 0..count {
data.extend_from_slice(unit);
}
Ok(Value::heap_string(
crate::heap_types::LispString::from_emacs_bytes(data),
))
} else {
if ch > 0xff {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), args[1]],
));
}
let data = vec![ch as u8; count];
Ok(Value::heap_string(
crate::heap_types::LispString::from_unibyte(data),
))
}
}
pub(crate) fn builtin_string(args: Vec<Value>) -> EvalResult {
builtin_string_slice(&args)
}
pub(crate) fn builtin_string_slice(args: &[Value]) -> EvalResult {
use crate::emacs_core::emacs_char;
let mut result = Vec::new();
for arg in args {
match arg.kind() {
ValueKind::Fixnum(c) => {
let mut buf = [0u8; emacs_char::MAX_MULTIBYTE_LENGTH];
let len = emacs_char::char_string(c as u32, &mut buf);
result.extend_from_slice(&buf[..len]);
}
_other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("characterp"), *arg],
));
}
}
}
Ok(Value::heap_string(
crate::heap_types::LispString::from_emacs_bytes(result),
))
}
pub(crate) fn builtin_unibyte_string(args: Vec<Value>) -> EvalResult {
let mut bytes = Vec::with_capacity(args.len());
for arg in args {
let n = match arg.kind() {
ValueKind::Fixnum(v) => v,
other => {
return Err(signal(
"wrong-type-argument",
vec![Value::symbol("integerp"), arg],
));
}
};
if !(0..=255).contains(&n) {
return Err(signal(
"args-out-of-range",
vec![Value::fixnum(n), Value::fixnum(0), Value::fixnum(255)],
));
}
bytes.push(n as u8);
}
Ok(Value::heap_string(
crate::heap_types::LispString::from_unibyte(bytes),
))
}
pub(crate) fn builtin_byte_to_string(args: Vec<Value>) -> EvalResult {
expect_args("byte-to-string", &args, 1)?;
let byte = expect_fixnum(&args[0])?;
if !(0..=255).contains(&byte) {
return Err(signal("error", vec![Value::string("Invalid byte")]));
}
Ok(Value::heap_string(
crate::heap_types::LispString::from_unibyte(vec![byte as u8]),
))
}
pub(crate) fn builtin_bitmap_spec_p(args: Vec<Value>) -> EvalResult {
expect_args("bitmap-spec-p", &args, 1)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_clear_face_cache(args: Vec<Value>) -> EvalResult {
expect_max_args("clear-face-cache", &args, 1)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_clear_buffer_auto_save_failure(args: Vec<Value>) -> EvalResult {
expect_args("clear-buffer-auto-save-failure", &args, 0)?;
Ok(Value::NIL)
}
pub(crate) fn builtin_string_width(args: Vec<Value>) -> EvalResult {
expect_min_args("string-width", &args, 1)?;
expect_max_args("string-width", &args, 3)?;
let ls = args[0].as_lisp_string().ok_or_else(|| {
signal(
"wrong-type-argument",
vec![Value::symbol("stringp"), args[0]],
)
})?;
let data = ls.as_bytes();
let is_multibyte = ls.is_multibyte();
if args.len() <= 1
|| (args.len() == 2 && args[1] == Value::NIL)
|| (args.len() <= 3
&& (args.len() < 2 || args[1] == Value::NIL || args[1] == Value::fixnum(0))
&& (args.len() < 3 || args[2] == Value::NIL))
{
return Ok(Value::fixnum(
super::super::string_escape::display_width_emacs(data, is_multibyte) as i64,
));
}
let units = super::super::string_escape::decode_units_emacs(data, is_multibyte);
let from = if args.len() > 1 && args[1] != Value::NIL {
expect_int(&args[1])? as usize
} else {
0
};
let to = if args.len() > 2 && args[2] != Value::NIL {
expect_int(&args[2])? as usize
} else {
units.len()
};
let width: usize = units
.iter()
.skip(from)
.take(to.saturating_sub(from))
.map(|(_, w)| w)
.sum();
Ok(Value::fixnum(width as i64))
}