use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::Write as _;
use super::chartable::{bool_vector_length, char_table_external_slots};
use super::expr::{self, Expr};
use super::intern::{SymId, lookup_interned, resolve_sym};
use super::string_escape::{
format_lisp_string, format_lisp_string_bytes, format_lisp_string_bytes_inner,
format_lisp_string_with_options,
};
use super::value::{
HashTableTest, StringTextPropertyRun, Value, get_string_text_properties_for_value, list_to_vec,
};
use crate::emacs_core::value::{ValueKind, VecLikeType};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct PrintOptions {
pub print_gensym: bool,
pub print_circle: bool,
pub print_escape_newlines: bool,
pub print_escape_nonascii: bool,
pub print_escape_multibyte: bool,
pub print_escape_control_characters: bool,
pub print_level: Option<i64>,
pub print_length: Option<i64>,
backquote_output_level: usize,
}
impl PrintOptions {
pub const fn with_print_gensym(print_gensym: bool) -> Self {
Self {
print_gensym,
print_circle: false,
print_escape_newlines: false,
print_escape_nonascii: false,
print_escape_multibyte: false,
print_escape_control_characters: false,
print_level: None,
print_length: None,
backquote_output_level: 0,
}
}
pub fn new(
print_gensym: bool,
print_circle: bool,
print_level: Option<i64>,
print_length: Option<i64>,
) -> Self {
Self {
print_gensym,
print_circle,
print_escape_newlines: false,
print_escape_nonascii: false,
print_escape_multibyte: false,
print_escape_control_characters: false,
print_level,
print_length,
backquote_output_level: 0,
}
}
pub(crate) fn enter_backquote(self) -> Self {
Self {
backquote_output_level: self.backquote_output_level + 1,
..self
}
}
pub(crate) fn exit_backquote(self) -> Self {
Self {
backquote_output_level: self.backquote_output_level.saturating_sub(1),
..self
}
}
pub(crate) fn allow_unquote_shorthand(self) -> bool {
self.backquote_output_level > 0
}
}
pub struct PrintCircleState {
number_table: HashMap<u64, i64>,
next_label: i64,
}
impl PrintCircleState {
fn new() -> Self {
Self {
number_table: HashMap::new(),
next_label: 0,
}
}
}
pub(crate) struct PrintState<'a> {
pub options: PrintOptions,
pub circle: Option<&'a mut PrintCircleState>,
pub buffers: Option<&'a crate::buffer::BufferManager>,
pub depth: i64,
}
fn is_print_circle_candidate(value: &Value, print_gensym: bool) -> bool {
match value.kind() {
ValueKind::Cons => true,
ValueKind::Veclike(VecLikeType::Vector) => {
value.as_vector_data().map_or(false, |v| !v.is_empty())
}
ValueKind::Veclike(VecLikeType::Record) => true,
ValueKind::Veclike(VecLikeType::HashTable) => true,
ValueKind::String => {
value.as_str().map_or(false, |s| !s.is_empty())
}
ValueKind::Symbol(id) if print_gensym => {
let name = resolve_sym(id);
lookup_interned(name) != Some(id)
}
_ => false,
}
}
fn object_identity_key(value: &Value) -> Option<u64> {
match value.kind() {
ValueKind::Cons
| ValueKind::Veclike(VecLikeType::Vector)
| ValueKind::Veclike(VecLikeType::Record)
| ValueKind::Veclike(VecLikeType::HashTable)
| ValueKind::String
| ValueKind::Veclike(VecLikeType::Lambda)
| ValueKind::Veclike(VecLikeType::ByteCode) => Some(value.0 as u64),
ValueKind::Symbol(id) => {
Some((1u64 << 63) | (id.0 as u64))
}
_ => None,
}
}
fn print_preprocess(value: &Value, state: &mut PrintCircleState, print_gensym: bool) {
let mut stack: Vec<Value> = vec![*value];
while let Some(obj) = stack.pop() {
if !is_print_circle_candidate(&obj, print_gensym) {
continue;
}
let key = match object_identity_key(&obj) {
Some(k) => k,
None => continue,
};
if let Some(status) = state.number_table.get(&key) {
if *status == 0 {
state.next_label += 1;
state.number_table.insert(key, -(state.next_label));
}
continue;
}
state.number_table.insert(key, 0);
match obj.kind() {
ValueKind::Cons => {
let pair_car = obj.cons_car();
let pair_cdr = obj.cons_cdr();
stack.push(pair_cdr);
stack.push(pair_car);
}
ValueKind::Veclike(VecLikeType::Vector) | ValueKind::Veclike(VecLikeType::Record) => {
let items = obj.as_vector_data().unwrap().clone();
for item in items.iter().rev() {
stack.push(*item);
}
}
ValueKind::Veclike(VecLikeType::HashTable) => {
let table = obj.as_hash_table().unwrap().clone();
for key_hk in table.insertion_order.iter().rev() {
if let Some(val) = table.data.get(key_hk) {
stack.push(*val);
let key_val = super::hashtab::hash_key_to_visible_value(&table, key_hk);
stack.push(key_val);
}
}
}
_ => {}
}
}
state.number_table.retain(|_, v| *v != 0);
}
pub(crate) fn print_value_stateful(value: &Value, options: PrintOptions) -> String {
print_value_stateful_with_buffers(value, None, options)
}
pub(crate) fn print_value_stateful_with_buffers(
value: &Value,
buffers: Option<&crate::buffer::BufferManager>,
options: PrintOptions,
) -> String {
let mut out = String::new();
if options.print_circle {
let mut circle = PrintCircleState::new();
print_preprocess(value, &mut circle, options.print_gensym);
let mut state = PrintState {
options,
circle: Some(&mut circle),
buffers,
depth: 0,
};
write_value_stateful(value, &mut out, &mut state);
} else {
let mut state = PrintState {
options,
circle: None,
buffers,
depth: 0,
};
write_value_stateful(value, &mut out, &mut state);
}
out
}
fn write_value_stateful(value: &Value, out: &mut String, state: &mut PrintState) {
if let Some(handle) = print_special_handle(value, state.buffers) {
out.push_str(&handle);
return;
}
if is_print_circle_candidate(value, state.options.print_gensym) {
if let Some(ref mut circle) = state.circle {
if let Some(key) = object_identity_key(value) {
if let Some(label) = circle.number_table.get_mut(&key) {
if *label < 0 {
write!(out, "#{}=", -(*label)).unwrap();
*label = -(*label); } else if *label > 0 {
write!(out, "#{}#", *label).unwrap();
return;
}
}
}
}
}
match value.kind() {
ValueKind::Nil => out.push_str("nil"),
ValueKind::T => out.push_str("t"),
ValueKind::Fixnum(v) => write!(out, "{}", v).unwrap(),
ValueKind::Float => out.push_str(&format_float(value.xfloat())),
ValueKind::Symbol(id) => out.push_str(&format_symbol(id, state.options)),
ValueKind::String => {
let s = value.as_str().unwrap().to_owned();
match get_string_text_properties_for_value(*value) {
Some(runs) => {
out.push_str(&format_lisp_propertized_string(&s, &runs, state.options))
}
None => out.push_str(&format_lisp_string_with_options(&s, &state.options)),
}
}
ValueKind::Cons => {
if let Some(level) = state.options.print_level {
if state.depth >= level {
out.push_str("#");
return;
}
}
if let Some(shorthand) = write_list_shorthand_stateful(value, state) {
out.push_str(&shorthand);
return;
}
state.depth += 1;
out.push('(');
write_cons_stateful(value, out, state);
out.push(')');
state.depth -= 1;
}
ValueKind::Veclike(VecLikeType::Vector) => {
if let Some(nbits) = bool_vector_length(value) {
out.push_str(&format_bool_vector(value, nbits as usize));
return;
}
if let Some(slots) = char_table_external_slots(value) {
if let Some(level) = state.options.print_level {
if state.depth >= level {
out.push_str("#");
return;
}
}
state.depth += 1;
out.push_str("#^[");
for (idx, item) in slots.iter().enumerate() {
if let Some(length) = state.options.print_length {
if idx as i64 >= length {
if idx > 0 {
out.push(' ');
}
out.push_str("...");
break;
}
}
if idx > 0 {
out.push(' ');
}
write_value_stateful(item, out, state);
}
out.push(']');
state.depth -= 1;
return;
}
if let Some(level) = state.options.print_level {
if state.depth >= level {
out.push_str("#");
return;
}
}
state.depth += 1;
out.push('[');
let items = value.as_vector_data().unwrap().clone();
for (idx, item) in items.iter().enumerate() {
if let Some(length) = state.options.print_length {
if idx as i64 >= length {
if idx > 0 {
out.push(' ');
}
out.push_str("...");
break;
}
}
if idx > 0 {
out.push(' ');
}
write_value_stateful(item, out, state);
}
out.push(']');
state.depth -= 1;
}
ValueKind::Veclike(VecLikeType::Record) => {
if let Some(level) = state.options.print_level {
if state.depth >= level {
out.push_str("#");
return;
}
}
state.depth += 1;
out.push_str("#s(");
let items = value.as_record_data().unwrap().clone();
for (idx, item) in items.iter().enumerate() {
if let Some(length) = state.options.print_length {
if idx as i64 >= length {
if idx > 0 {
out.push(' ');
}
out.push_str("...");
break;
}
}
if idx > 0 {
out.push(' ');
}
write_value_stateful(item, out, state);
}
out.push(')');
state.depth -= 1;
}
ValueKind::Veclike(VecLikeType::HashTable) => {
if let Some(level) = state.options.print_level {
if state.depth >= level {
out.push_str("#");
return;
}
}
state.depth += 1;
write_hash_table_stateful(value, out, state);
state.depth -= 1;
}
ValueKind::Veclike(VecLikeType::Lambda) => {
let obj_key = value.0;
let text = with_print_object_guard(
PrintObjectRef::Lambda(obj_key),
|index| format!("#{index}"),
|| {
if value.closure_env().flatten().is_some() {
return format_interpreted_closure(value, state.options);
}
if let Some(list_form) = crate::emacs_core::builtins::lambda_to_cons_list(value)
{
return print_value_with_options(&list_form, state.options);
}
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, state.options))
.unwrap_or_else(|| "nil".to_string());
format!("(lambda {} {})", params, body)
},
);
out.push_str(&text);
}
ValueKind::Veclike(VecLikeType::Macro) => {
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, state.options))
.unwrap_or_else(|| "nil".to_string());
write!(out, "(macro {} {})", params, body).unwrap();
}
ValueKind::Veclike(VecLikeType::Subr) => {
let id = value.as_subr_id().unwrap();
write!(out, "#<subr {}>", resolve_sym(id)).unwrap()
}
ValueKind::Veclike(VecLikeType::ByteCode) => {
let bc = value.get_bytecode_data().unwrap();
let params = format_params(&bc.params);
write!(out, "#<bytecode {} ({} ops)>", params, bc.ops.len()).unwrap();
}
ValueKind::Veclike(VecLikeType::Marker) => out.push_str(
&print_special_handle(value, state.buffers).unwrap_or_else(|| "#<marker>".to_string()),
),
ValueKind::Veclike(VecLikeType::Overlay) => out.push_str(
&print_special_handle(value, state.buffers).unwrap_or_else(|| "#<overlay>".to_string()),
),
ValueKind::Veclike(VecLikeType::Buffer) => {
let bid = value.as_buffer_id().unwrap();
write!(out, "#<buffer {}>", bid.0).unwrap();
}
ValueKind::Veclike(VecLikeType::Window) => {
let wid = value.as_window_id().unwrap();
write!(out, "#<window {}>", wid).unwrap();
}
ValueKind::Veclike(VecLikeType::Frame) => {
let fid = value.as_frame_id().unwrap();
out.push_str(&format_frame_handle(fid));
}
ValueKind::Veclike(VecLikeType::Timer) => {
let tid = value.as_timer_id().unwrap();
write!(out, "#<timer {}>", tid).unwrap();
}
ValueKind::Unknown => write!(out, "#<unknown {:#x}>", value.0).unwrap(),
}
}
fn write_list_shorthand_stateful(value: &Value, state: &mut PrintState) -> Option<String> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
let head = match items[0].kind() {
ValueKind::Symbol(id) => resolve_sym(id),
_ => return None,
};
if head == "make-hash-table-from-literal" {
if let Some(payload) = quote_payload_stateful(&items[1]) {
let mut out = String::from("#s");
write_value_stateful(&payload, &mut out, state);
return Some(out);
}
return None;
}
let (prefix, nested_options) = match head {
"quote" => ("'", state.options),
"function" => ("#'", state.options),
"`" => ("`", state.options.enter_backquote()),
"," => {
if !state.options.allow_unquote_shorthand() {
return None;
}
(",", state.options.exit_backquote())
}
",@" => {
if !state.options.allow_unquote_shorthand() {
return None;
}
(",@", state.options.exit_backquote())
}
_ => return None,
};
let saved_options = state.options;
state.options = nested_options;
let mut out = String::from(prefix);
write_value_stateful(&items[1], &mut out, state);
state.options = saved_options;
Some(out)
}
fn quote_payload_stateful(value: &Value) -> Option<Value> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
match items[0].kind() {
ValueKind::Symbol(id) if resolve_sym(id) == "quote" => Some(items[1]),
_ => None,
}
}
fn write_cons_stateful(value: &Value, out: &mut String, state: &mut PrintState) {
let mut cursor = *value;
let mut first = true;
let mut count: i64 = 0;
loop {
match cursor.kind() {
ValueKind::Cons => {
if let Some(length) = state.options.print_length {
if count >= length {
if !first {
out.push(' ');
}
out.push_str("...");
return;
}
}
if !first {
out.push(' ');
}
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
write_value_stateful(&pair_car, out, state);
cursor = pair_cdr;
first = false;
count += 1;
if cursor.is_cons() {
if let Some(ref circle) = state.circle {
if let Some(key) = object_identity_key(&cursor) {
if let Some(label) = circle.number_table.get(&key) {
if *label != 0 {
out.push_str(" . ");
write_value_stateful(&cursor, out, state);
return;
}
}
}
}
}
}
ValueKind::Nil => return,
_ => {
if !first {
out.push_str(" . ");
}
write_value_stateful(&cursor, out, state);
return;
}
}
}
}
fn write_hash_table_stateful(value: &Value, out: &mut String, state: &mut PrintState) {
let table = value.as_hash_table().unwrap().clone();
out.push_str("#s(hash-table");
match table.test {
HashTableTest::Eq => out.push_str(" test eq"),
HashTableTest::Equal => out.push_str(" test equal"),
HashTableTest::Eql => {}
}
if let Some(ref weakness) = table.weakness {
let name = match weakness {
super::value::HashTableWeakness::Key => "key",
super::value::HashTableWeakness::Value => "value",
super::value::HashTableWeakness::KeyOrValue => "key-or-value",
super::value::HashTableWeakness::KeyAndValue => "key-and-value",
};
out.push_str(" weakness ");
out.push_str(name);
}
if !table.data.is_empty() {
out.push_str(" data (");
let mut first = true;
let mut count: i64 = 0;
for key in &table.insertion_order {
if let Some(val) = table.data.get(key) {
if let Some(length) = state.options.print_length {
if count >= length {
if !first {
out.push(' ');
}
out.push_str("...");
break;
}
}
if !first {
out.push(' ');
}
let key_val = super::hashtab::hash_key_to_visible_value(&table, key);
write_value_stateful(&key_val, out, state);
out.push(' ');
write_value_stateful(val, out, state);
first = false;
count += 1;
}
}
out.push(')');
}
out.push(')');
}
thread_local! {
static PRINT_OBJECT_STACK: RefCell<Vec<PrintObjectRef>> = const { RefCell::new(Vec::new()) };
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PrintObjectRef {
Lambda(usize),
}
fn with_print_object_guard<R>(
object: PrintObjectRef,
on_cycle: impl FnOnce(usize) -> R,
render: impl FnOnce() -> R,
) -> R {
PRINT_OBJECT_STACK.with(|stack| {
if let Some(index) = stack.borrow().iter().position(|entry| *entry == object) {
return on_cycle(index);
}
stack.borrow_mut().push(object);
let rendered = render();
stack.borrow_mut().pop();
rendered
})
}
fn format_marker_handle(
value: &Value,
buffers: Option<&crate::buffer::BufferManager>,
) -> Option<String> {
if !super::marker::is_marker(value) {
return None;
}
if !value.is_marker() {
return None;
};
let marker = value.as_marker_data().unwrap().clone();
let buffer_name = marker
.buffer
.and_then(|buffer_id| buffers.and_then(|manager| manager.get(buffer_id)))
.map(|buffer| buffer.name.clone());
let mut out = String::from("#<marker ");
if marker.insertion_type {
out.push_str("(moves after insertion) ");
}
if let Some(name) = buffer_name.as_deref() {
if let Some(pos) = marker.position {
out.push_str(&format!("at {pos} in {name}"));
} else {
out.push_str("in no buffer");
}
} else {
out.push_str("in no buffer");
}
out.push('>');
Some(out)
}
fn format_overlay_handle(
value: &Value,
buffers: Option<&crate::buffer::BufferManager>,
) -> Option<String> {
if !value.is_overlay() {
return None;
};
let overlay = value.as_overlay_data().unwrap().clone();
let Some(buffer_id) = overlay.buffer else {
return Some("#<overlay in no buffer>".to_string());
};
let Some(buffers) = buffers else {
return Some(format!(
"#<overlay from {} to {}>",
overlay.start, overlay.end
));
};
let Some(buffer) = buffers.get(buffer_id) else {
return Some("#<overlay in no buffer>".to_string());
};
Some(format!(
"#<overlay from {} to {} in {}>",
buffer.text.byte_to_char(overlay.start) + 1,
buffer.text.byte_to_char(overlay.end) + 1,
buffer.name
))
}
fn print_special_handle(
value: &Value,
buffers: Option<&crate::buffer::BufferManager>,
) -> Option<String> {
super::terminal::pure::print_terminal_handle(value)
.or_else(|| format_marker_handle(value, buffers))
.or_else(|| format_overlay_handle(value, buffers))
}
fn format_frame_handle(id: u64) -> String {
if id >= crate::window::FRAME_ID_BASE {
let ordinal = id - crate::window::FRAME_ID_BASE + 1;
format!("#<frame F{} 0x{:x}>", ordinal, id)
} else {
format!("#<frame {}>", id)
}
}
fn format_lisp_propertized_string(
s: &str,
runs: &[StringTextPropertyRun],
options: PrintOptions,
) -> String {
let mut out = String::from("#(");
out.push_str(&format_lisp_string_with_options(s, &options));
for run in runs {
out.push(' ');
out.push_str(&run.start.to_string());
out.push(' ');
out.push_str(&run.end.to_string());
out.push(' ');
out.push_str(&print_value_with_options(&run.plist, options));
}
out.push(')');
out
}
pub fn print_value_with_buffers(value: &Value, buffers: &crate::buffer::BufferManager) -> String {
print_value_with_buffers_and_options(value, buffers, PrintOptions::default())
}
pub fn print_value_with_buffers_and_options(
value: &Value,
buffers: &crate::buffer::BufferManager,
options: PrintOptions,
) -> String {
if options.print_circle || options.print_level.is_some() || options.print_length.is_some() {
return print_value_stateful_with_buffers(value, Some(buffers), options);
}
if let Some(handle) = print_special_handle(value, Some(buffers)) {
return handle;
}
match value.kind() {
ValueKind::Veclike(VecLikeType::Buffer) => {
let bid = value.as_buffer_id().unwrap();
if let Some(buf) = buffers.get(bid) {
return format!("#<buffer {}>", buf.name);
}
if buffers.dead_buffer_last_name(bid).is_some() {
return "#<killed buffer>".to_string();
}
format!("#<buffer {}>", bid.0)
}
ValueKind::Cons => {
if let Some(shorthand) = print_list_shorthand_with_buffers(value, buffers, options) {
return shorthand;
}
let mut out = String::from("(");
print_cons_with_buffers(value, &mut out, buffers, options);
out.push(')');
out
}
ValueKind::Veclike(VecLikeType::Vector) => {
if let Some(nbits) = super::chartable::bool_vector_length(value) {
return format_bool_vector(value, nbits as usize);
}
if let Some(slots) = char_table_external_slots(value) {
let parts: Vec<String> = slots
.iter()
.map(|v| print_value_with_buffers_and_options(v, buffers, options))
.collect();
return format!("#^[{}]", parts.join(" "));
}
let items = value.as_vector_data().unwrap().clone();
let parts: Vec<String> = items
.iter()
.map(|v| print_value_with_buffers_and_options(v, buffers, options))
.collect();
format!("[{}]", parts.join(" "))
}
ValueKind::Veclike(VecLikeType::Marker) => {
print_special_handle(value, Some(buffers)).unwrap_or_else(|| "#<marker>".to_string())
}
ValueKind::Veclike(VecLikeType::Overlay) => {
print_special_handle(value, Some(buffers)).unwrap_or_else(|| "#<overlay>".to_string())
}
_ => print_value_with_options(value, options),
}
}
fn print_list_shorthand_with_buffers(
value: &Value,
buffers: &crate::buffer::BufferManager,
options: PrintOptions,
) -> Option<String> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
let head = match items[0].kind() {
ValueKind::Symbol(id) => resolve_sym(id),
_ => return None,
};
if head == "make-hash-table-from-literal" {
if let Some(payload) = quote_payload(&items[1]) {
return Some(format!(
"#s{}",
print_value_with_buffers_and_options(&payload, buffers, options)
));
}
return None;
}
let (prefix, nested_options) = match head {
"quote" => ("'", options),
"function" => ("#'", options),
"`" => ("`", options.enter_backquote()),
"," => {
if !options.allow_unquote_shorthand() {
return None;
}
(",", options.exit_backquote())
}
",@" => {
if !options.allow_unquote_shorthand() {
return None;
}
(",@", options.exit_backquote())
}
_ => return None,
};
Some(format!(
"{prefix}{}",
print_value_with_buffers_and_options(&items[1], buffers, nested_options)
))
}
fn print_cons_with_buffers(
value: &Value,
out: &mut String,
buffers: &crate::buffer::BufferManager,
options: PrintOptions,
) {
let mut cursor = *value;
let mut first = true;
loop {
match cursor.kind() {
ValueKind::Cons => {
if !first {
out.push(' ');
}
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
out.push_str(&print_value_with_buffers_and_options(
&pair_car, buffers, options,
));
cursor = pair_cdr;
first = false;
}
ValueKind::Nil => return,
_ => {
if !first {
out.push_str(" . ");
}
out.push_str(&print_value_with_buffers_and_options(
&cursor, buffers, options,
));
return;
}
}
}
}
pub fn print_value(value: &Value) -> String {
print_value_with_options(value, PrintOptions::default())
}
pub fn print_value_with_options(value: &Value, options: PrintOptions) -> String {
if options.print_circle || options.print_level.is_some() || options.print_length.is_some() {
return print_value_stateful(value, options);
}
if let Some(handle) = print_special_handle(value, None) {
return handle;
}
match value.kind() {
ValueKind::Nil => "nil".to_string(),
ValueKind::T => "t".to_string(),
ValueKind::Fixnum(v) => v.to_string(),
ValueKind::Float => format_float(value.xfloat()),
ValueKind::Symbol(id) => format_symbol(id, options),
ValueKind::String => {
let s = value.as_str().unwrap().to_owned();
match get_string_text_properties_for_value(*value) {
Some(runs) => format_lisp_propertized_string(&s, &runs, options),
None => format_lisp_string_with_options(&s, &options),
}
}
ValueKind::Cons => {
if let Some(shorthand) = print_list_shorthand(value, options) {
return shorthand;
}
let mut out = String::from("(");
print_cons(value, &mut out, options);
out.push(')');
out
}
ValueKind::Veclike(VecLikeType::Vector) => {
if let Some(nbits) = bool_vector_length(value) {
return format_bool_vector(value, nbits as usize);
}
if let Some(slots) = char_table_external_slots(value) {
let parts: Vec<String> = slots
.iter()
.map(|item| print_value_with_options(item, options))
.collect();
return format!("#^[{}]", parts.join(" "));
}
let items = value.as_vector_data().unwrap().clone();
let parts: Vec<String> = items
.iter()
.map(|item| print_value_with_options(item, options))
.collect();
format!("[{}]", parts.join(" "))
}
ValueKind::Veclike(VecLikeType::Record) => {
let items = value.as_record_data().unwrap().clone();
let parts: Vec<String> = items
.iter()
.map(|item| print_value_with_options(item, options))
.collect();
format!("#s({})", parts.join(" "))
}
ValueKind::Veclike(VecLikeType::HashTable) => format_hash_table(value, options),
ValueKind::Veclike(VecLikeType::Lambda) => with_print_object_guard(
PrintObjectRef::Lambda(value.0),
|index| format!("#{index}"),
|| {
if value.closure_env().flatten().is_some() {
return format_interpreted_closure(value, options);
}
if let Some(list_form) = crate::emacs_core::builtins::lambda_to_cons_list(value) {
return print_value_with_options(&list_form, options);
}
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, options))
.unwrap_or_else(|| "nil".to_string());
format!("(lambda {} {})", params, body)
},
),
ValueKind::Veclike(VecLikeType::Macro) => {
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, options))
.unwrap_or_else(|| "nil".to_string());
format!("(macro {} {})", params, body)
}
ValueKind::Veclike(VecLikeType::Subr) => {
let id = value.as_subr_id().unwrap();
format!("#<subr {}>", resolve_sym(id))
}
ValueKind::Veclike(VecLikeType::ByteCode) => {
let bc = value.get_bytecode_data().unwrap();
let params = format_params(&bc.params);
format!("#<bytecode {} ({} ops)>", params, bc.ops.len())
}
ValueKind::Veclike(VecLikeType::Marker) => {
print_special_handle(value, None).unwrap_or_else(|| "#<marker>".to_string())
}
ValueKind::Veclike(VecLikeType::Overlay) => {
print_special_handle(value, None).unwrap_or_else(|| "#<overlay>".to_string())
}
ValueKind::Veclike(VecLikeType::Buffer) => {
format!("#<buffer {}>", value.as_buffer_id().unwrap().0)
}
ValueKind::Veclike(VecLikeType::Window) => {
format!("#<window {}>", value.as_window_id().unwrap())
}
ValueKind::Veclike(VecLikeType::Frame) => format_frame_handle(value.as_frame_id().unwrap()),
ValueKind::Veclike(VecLikeType::Timer) => {
format!("#<timer {}>", value.as_timer_id().unwrap())
}
ValueKind::Unknown => format!("#<unknown {:#x}>", value.0),
}
}
pub fn print_value_bytes(value: &Value) -> Vec<u8> {
print_value_bytes_with_options(value, PrintOptions::default())
}
pub fn print_value_bytes_with_options(value: &Value, options: PrintOptions) -> Vec<u8> {
if options.print_circle || options.print_level.is_some() || options.print_length.is_some() {
return print_value_stateful(value, options).into_bytes();
}
let mut out = Vec::new();
append_print_value_bytes(value, &mut out, options);
out
}
fn append_print_value_bytes(value: &Value, out: &mut Vec<u8>, options: PrintOptions) {
if let Some(handle) = print_special_handle(value, None) {
out.extend_from_slice(handle.as_bytes());
return;
}
match value.kind() {
ValueKind::Nil => out.extend_from_slice(b"nil"),
ValueKind::T => out.extend_from_slice(b"t"),
ValueKind::Fixnum(v) => out.extend_from_slice(v.to_string().as_bytes()),
ValueKind::Float => out.extend_from_slice(format_float(value.xfloat()).as_bytes()),
ValueKind::Symbol(id) => append_symbol_bytes(id, out, options),
ValueKind::String => {
let s = value.as_str().unwrap().to_owned();
let str_bytes = format_lisp_string_bytes_inner(&s, &options);
if let Some(runs) = get_string_text_properties_for_value(*value) {
out.extend_from_slice(b"#(");
out.extend_from_slice(&str_bytes);
for run in runs {
out.push(b' ');
out.extend_from_slice(run.start.to_string().as_bytes());
out.push(b' ');
out.extend_from_slice(run.end.to_string().as_bytes());
out.push(b' ');
append_print_value_bytes(&run.plist, out, options);
}
out.push(b')');
} else {
out.extend_from_slice(&str_bytes);
}
}
ValueKind::Cons => {
if let Some(shorthand) = print_list_shorthand_bytes(value, options) {
out.extend_from_slice(&shorthand);
return;
}
out.push(b'(');
print_cons_bytes(value, out, options);
out.push(b')');
}
ValueKind::Veclike(VecLikeType::Vector) => {
if let Some(nbits) = bool_vector_length(value) {
append_bool_vector_bytes(value, nbits as usize, out);
return;
}
if let Some(slots) = char_table_external_slots(value) {
out.extend_from_slice(b"#^[");
for (idx, item) in slots.iter().enumerate() {
if idx > 0 {
out.push(b' ');
}
append_print_value_bytes(item, out, options);
}
out.push(b']');
return;
}
out.push(b'[');
let items = value.as_vector_data().unwrap().clone();
for (idx, item) in items.iter().enumerate() {
if idx > 0 {
out.push(b' ');
}
append_print_value_bytes(item, out, options);
}
out.push(b']');
}
ValueKind::Veclike(VecLikeType::Record) => {
out.extend_from_slice(b"#s(");
let items = value.as_record_data().unwrap().clone();
for (idx, item) in items.iter().enumerate() {
if idx > 0 {
out.push(b' ');
}
append_print_value_bytes(item, out, options);
}
out.push(b')');
}
ValueKind::Veclike(VecLikeType::HashTable) => {
out.extend_from_slice(format_hash_table(value, options).as_bytes());
}
ValueKind::Veclike(VecLikeType::Lambda) => {
let text = with_print_object_guard(
PrintObjectRef::Lambda(value.0),
|index| format!("#{index}"),
|| {
if value.closure_env().flatten().is_some() {
format_interpreted_closure(value, options)
} else {
if let Some(list_form) =
crate::emacs_core::builtins::lambda_to_cons_list(value)
{
return print_value_with_options(&list_form, options);
}
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, options))
.unwrap_or_else(|| "nil".to_string());
format!("(lambda {} {})", params, body)
}
},
);
out.extend_from_slice(text.as_bytes());
}
ValueKind::Veclike(VecLikeType::Macro) => {
let params = value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params);
let body = value
.closure_body_value()
.map(|body| format_closure_body_forms(body, options))
.unwrap_or_else(|| "nil".to_string());
out.extend_from_slice(format!("(macro {} {})", params, body).as_bytes());
}
ValueKind::Veclike(VecLikeType::Subr) => {
let id = value.as_subr_id().unwrap();
out.extend_from_slice(format!("#<subr {}>", resolve_sym(id)).as_bytes())
}
ValueKind::Veclike(VecLikeType::ByteCode) => {
let bc = value.get_bytecode_data().unwrap();
let params = format_params(&bc.params);
out.extend_from_slice(
format!("#<bytecode {} ({} ops)>", params, bc.ops.len()).as_bytes(),
);
}
ValueKind::Veclike(VecLikeType::Marker) => out.extend_from_slice(
print_special_handle(value, None)
.unwrap_or_else(|| "#<marker>".to_string())
.as_bytes(),
),
ValueKind::Veclike(VecLikeType::Overlay) => out.extend_from_slice(
print_special_handle(value, None)
.unwrap_or_else(|| "#<overlay>".to_string())
.as_bytes(),
),
ValueKind::Veclike(VecLikeType::Buffer) => {
out.extend_from_slice(
format!("#<buffer {}>", value.as_buffer_id().unwrap().0).as_bytes(),
);
}
ValueKind::Veclike(VecLikeType::Window) => {
out.extend_from_slice(
format!("#<window {}>", value.as_window_id().unwrap()).as_bytes(),
);
}
ValueKind::Veclike(VecLikeType::Frame) => {
out.extend_from_slice(format_frame_handle(value.as_frame_id().unwrap()).as_bytes());
}
ValueKind::Veclike(VecLikeType::Timer) => {
out.extend_from_slice(format!("#<timer {}>", value.as_timer_id().unwrap()).as_bytes());
}
ValueKind::Unknown => {
out.extend_from_slice(format!("#<unknown {:#x}>", value.0).as_bytes());
}
}
}
pub fn print_expr(expr: &Expr) -> String {
expr::print_expr(expr)
}
fn format_symbol(id: super::intern::SymId, options: PrintOptions) -> String {
let name = resolve_sym(id);
let canonical = lookup_interned(name);
if canonical == Some(id) {
format_symbol_name(name)
} else if options.print_gensym {
if name.is_empty() {
"#:".to_string()
} else {
format!("#:{}", format_symbol_name(name))
}
} else {
format_symbol_name(name)
}
}
fn append_symbol_bytes(id: super::intern::SymId, out: &mut Vec<u8>, options: PrintOptions) {
out.extend_from_slice(format_symbol(id, options).as_bytes());
}
fn format_symbol_name(name: &str) -> String {
if name.is_empty() {
return "##".to_string();
}
let mut out = String::with_capacity(name.len());
for (idx, ch) in name.chars().enumerate() {
let needs_escape = matches!(
ch,
' ' | '\t'
| '\n'
| '\r'
| '\u{0c}'
| '('
| ')'
| '['
| ']'
| '"'
| '\\'
| ';'
| '#'
| '\''
| '`'
| ','
) || (idx == 0 && matches!(ch, '.' | '?'));
if needs_escape {
out.push('\\');
}
out.push(ch);
}
out
}
pub(crate) fn format_float(f: f64) -> String {
const NAN_QUIET_BIT: u64 = 1u64 << 51;
const NAN_PAYLOAD_MASK: u64 = (1u64 << 51) - 1;
if f.is_nan() {
let bits = f.to_bits();
let frac = bits & ((1u64 << 52) - 1);
if (frac & NAN_QUIET_BIT) != 0 {
let payload = frac & NAN_PAYLOAD_MASK;
return if f.is_sign_negative() {
format!("-{}.0e+NaN", payload)
} else {
format!("{}.0e+NaN", payload)
};
}
return if f.is_sign_negative() {
"-0.0e+NaN".to_string()
} else {
"0.0e+NaN".to_string()
};
}
if f.is_infinite() {
return if f > 0.0 {
"1.0e+INF".to_string()
} else {
"-1.0e+INF".to_string()
};
}
format_float_dtoastr(f)
}
fn format_float_dtoastr(f: f64) -> String {
let abs_f = f.abs();
let start_prec = if abs_f != 0.0 && abs_f < f64::MIN_POSITIVE {
1
} else {
15 };
for prec in start_prec..=20 {
let s = format!("{:.prec$e}", f, prec = prec - 1);
if let Ok(parsed) = s.parse::<f64>() {
if parsed.to_bits() == f.to_bits() {
return rust_sci_to_emacs_g(f, &s, prec);
}
}
}
let s = format!("{:.20e}", f);
rust_sci_to_emacs_g(f, &s, 21)
}
fn rust_sci_to_emacs_g(f: f64, sci: &str, prec: usize) -> String {
let (mantissa_str, exp_str) = sci.split_once('e').unwrap_or((sci, "0"));
let exp: i32 = exp_str.parse().unwrap_or(0);
let result = if exp >= -4 && exp < prec as i32 {
format_g_fixed(f, mantissa_str, exp, prec)
} else {
format_g_scientific(mantissa_str, exp, prec)
};
ensure_decimal_point(result)
}
fn format_g_fixed(f: f64, _mantissa: &str, exp: i32, prec: usize) -> String {
let digits_after_dot = (prec as i32 - exp - 1).max(0) as usize;
let s = format!("{:.digits$}", f, digits = digits_after_dot);
trim_trailing_zeros_g(&s)
}
fn format_g_scientific(mantissa: &str, exp: i32, _prec: usize) -> String {
let trimmed = trim_trailing_zeros_g(mantissa);
if exp >= 0 {
format!("{}e+{:02}", trimmed, exp)
} else {
format!("{}e-{:02}", trimmed, -exp)
}
}
fn trim_trailing_zeros_g(s: &str) -> String {
if !s.contains('.') {
return s.to_string();
}
let trimmed = s.trim_end_matches('0');
let trimmed = trimmed.trim_end_matches('.');
trimmed.to_string()
}
fn ensure_decimal_point(mut s: String) -> String {
let has_dot_or_exp = s.bytes().any(|b| b == b'.' || b == b'e' || b == b'E');
if !has_dot_or_exp {
s.push_str(".0");
} else if s.ends_with('.') {
s.push('0');
}
s
}
fn format_params(params: &super::value::LambdaParams) -> String {
let mut parts = Vec::new();
for p in ¶ms.required {
parts.push(resolve_sym(*p).to_string());
}
if !params.optional.is_empty() {
parts.push("&optional".to_string());
for p in ¶ms.optional {
parts.push(resolve_sym(*p).to_string());
}
}
if let Some(rest) = params.rest {
parts.push("&rest".to_string());
parts.push(resolve_sym(rest).to_string());
}
if parts.is_empty() {
"nil".to_string()
} else {
format!("({})", parts.join(" "))
}
}
fn format_closure_body_forms(body: Value, options: PrintOptions) -> String {
let Some(forms) = list_to_vec(&body) else {
return print_value_with_options(&body, options);
};
if forms.is_empty() {
"nil".to_string()
} else {
forms
.iter()
.map(|form| print_value_with_options(form, options))
.collect::<Vec<_>>()
.join(" ")
}
}
fn format_interpreted_closure(value: &Value, options: PrintOptions) -> String {
let mut slots = Vec::with_capacity(5);
slots.push(
value
.closure_params()
.map_or_else(|| "nil".to_string(), format_params),
);
slots.push(
value
.closure_body_value()
.map(|body| print_value_with_options(&body, options))
.unwrap_or_else(|| "nil".to_string()),
);
let env = value.closure_env().flatten().expect("closure env");
slots.push(if env == Value::NIL {
"(t)".to_string()
} else {
print_value_with_options(&env, options)
});
if let Some(doc_value) = value.closure_doc_value()
&& !doc_value.is_nil()
{
slots.push("nil".to_string());
slots.push(if doc_value.is_string() {
format_lisp_string(doc_value.as_str().unwrap())
} else {
print_value_with_options(&doc_value, options)
});
}
format!("#[{}]", slots.join(" "))
}
fn print_list_shorthand(value: &Value, options: PrintOptions) -> Option<String> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
let head = match items[0].kind() {
ValueKind::Symbol(id) => resolve_sym(id),
_ => return None,
};
if head == "make-hash-table-from-literal" {
if let Some(payload) = quote_payload(&items[1]) {
return Some(format!("#s{}", print_value_with_options(&payload, options)));
}
return None;
}
let (prefix, nested_options) = match head {
"quote" => ("'", options),
"function" => ("#'", options),
"`" => ("`", options.enter_backquote()),
"," => {
if !options.allow_unquote_shorthand() {
return None;
}
(",", options.exit_backquote())
}
",@" => {
if !options.allow_unquote_shorthand() {
return None;
}
(",@", options.exit_backquote())
}
_ => return None,
};
Some(format!(
"{prefix}{}",
print_value_with_options(&items[1], nested_options)
))
}
fn print_list_shorthand_bytes(value: &Value, options: PrintOptions) -> Option<Vec<u8>> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
let head = match items[0].kind() {
ValueKind::Symbol(id) => resolve_sym(id),
_ => return None,
};
if head == "make-hash-table-from-literal" {
let payload = quote_payload(&items[1])?;
let mut out = Vec::new();
out.extend_from_slice(b"#s");
append_print_value_bytes(&payload, &mut out, options);
return Some(out);
}
let (prefix, nested_options): (&[u8], PrintOptions) = match head {
"quote" => (b"'", options),
"function" => (b"#'", options),
"`" => (b"`", options.enter_backquote()),
"," => {
if !options.allow_unquote_shorthand() {
return None;
}
(b",", options.exit_backquote())
}
",@" => {
if !options.allow_unquote_shorthand() {
return None;
}
(b",@", options.exit_backquote())
}
_ => return None,
};
let mut out = Vec::new();
out.extend_from_slice(prefix);
append_print_value_bytes(&items[1], &mut out, nested_options);
Some(out)
}
fn quote_payload(value: &Value) -> Option<Value> {
let items = list_to_vec(value)?;
if items.len() != 2 {
return None;
}
match items[0].kind() {
ValueKind::Symbol(id) if resolve_sym(id) == "quote" => Some(items[1]),
_ => None,
}
}
fn print_cons(value: &Value, out: &mut String, options: PrintOptions) {
let mut cursor = *value;
let mut first = true;
loop {
match cursor.kind() {
ValueKind::Cons => {
if !first {
out.push(' ');
}
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
out.push_str(&print_value_with_options(&pair_car, options));
cursor = pair_cdr;
first = false;
}
ValueKind::Nil => return,
_ => {
if !first {
out.push_str(" . ");
}
out.push_str(&print_value_with_options(&cursor, options));
return;
}
}
}
}
fn print_cons_bytes(value: &Value, out: &mut Vec<u8>, options: PrintOptions) {
let mut cursor = *value;
let mut first = true;
loop {
match cursor.kind() {
ValueKind::Cons => {
if !first {
out.push(b' ');
}
let pair_car = cursor.cons_car();
let pair_cdr = cursor.cons_cdr();
append_print_value_bytes(&pair_car, out, options);
cursor = pair_cdr;
first = false;
}
ValueKind::Nil => return,
_ => {
if !first {
out.extend_from_slice(b" . ");
}
append_print_value_bytes(&cursor, out, options);
return;
}
}
}
}
fn format_bool_vector(value: &Value, nbits: usize) -> String {
let mut out = Vec::new();
append_bool_vector_bytes(value, nbits, &mut out);
String::from_utf8_lossy(&out).into_owned()
}
fn append_bool_vector_bytes(value: &Value, nbits: usize, out: &mut Vec<u8>) {
let items = match value.kind() {
ValueKind::Veclike(VecLikeType::Vector) => value.as_vector_data().unwrap().clone(),
_ => return,
};
let nbytes = (nbits + 7) / 8;
out.extend_from_slice(b"#&");
out.extend_from_slice(nbits.to_string().as_bytes());
out.push(b'"');
for byte_idx in 0..nbytes {
let mut byte_val: u8 = 0;
for bit_idx in 0..8 {
let overall_bit = byte_idx * 8 + bit_idx;
if overall_bit >= nbits {
break;
}
let is_set = match items.get(2 + overall_bit) {
Some(v) => match v.kind() {
ValueKind::Fixnum(n) => n != 0,
_ => v.is_truthy(),
},
None => false,
};
if is_set {
byte_val |= 1 << bit_idx; }
}
match byte_val {
b'"' => out.extend_from_slice(b"\\\""),
b'\\' => out.extend_from_slice(b"\\\\"),
b if b > 0x7F => {
out.extend_from_slice(format!("\\{:03o}", b).as_bytes());
}
_ => out.push(byte_val),
}
}
out.push(b'"');
}
fn format_hash_table(value: &Value, options: PrintOptions) -> String {
let table = value.as_hash_table().unwrap().clone();
let mut out = String::from("#s(hash-table");
match table.test {
HashTableTest::Eq => out.push_str(" test eq"),
HashTableTest::Equal => out.push_str(" test equal"),
HashTableTest::Eql => {} }
if let Some(ref weakness) = table.weakness {
let name = match weakness {
super::value::HashTableWeakness::Key => "key",
super::value::HashTableWeakness::Value => "value",
super::value::HashTableWeakness::KeyOrValue => "key-or-value",
super::value::HashTableWeakness::KeyAndValue => "key-and-value",
};
out.push_str(" weakness ");
out.push_str(name);
}
if !table.data.is_empty() {
out.push_str(" data (");
let mut first = true;
for key in &table.insertion_order {
if let Some(val) = table.data.get(key) {
if !first {
out.push(' ');
}
let key_val = super::hashtab::hash_key_to_visible_value(&table, key);
out.push_str(&print_value_with_options(&key_val, options));
out.push(' ');
out.push_str(&print_value_with_options(val, options));
first = false;
}
}
out.push(')');
}
out.push(')');
out
}
#[cfg(test)]
#[path = "print_test.rs"]
mod tests;