use indexmap::IndexMap;
use crate::{
error::{EvalError, InterpreterError},
eval::{
classes::{call_method, lookup_method_in_mro},
functions::CallArgs,
},
state::InterpreterState,
tools::Tools,
value::{DataclassField, Value},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenderMode {
Display,
Repr,
Ascii,
}
pub fn render<'a>(
state: &'a mut InterpreterState,
value: &'a Value,
mode: RenderMode,
tools: &'a Tools,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, EvalError>> + Send + 'a>> {
Box::pin(async move {
if let Value::Instance(inst) = value {
let slot_name = match mode {
RenderMode::Display => "__str__",
RenderMode::Repr | RenderMode::Ascii => "__repr__",
};
if let Some((_, method)) = lookup_method_in_mro(state, &inst.class_name, slot_name) {
let call = CallArgs { positional: &[], keyword: &IndexMap::new() };
let (returned, _self_after) =
call_method(state, &method, value.clone(), call, tools).await?;
let rendered = match &returned {
Value::String(s) => s.to_string(),
other => {
return Err(EvalError::from(InterpreterError::TypeError(format!(
"{}.{} returned non-string (type {})",
inst.class_name,
slot_name,
other.type_name()
))));
}
};
return Ok(match mode {
RenderMode::Ascii => ascii_escape(&rendered),
_ => rendered,
});
}
if matches!(mode, RenderMode::Display) {
if let Some((_, method)) = lookup_method_in_mro(state, &inst.class_name, "__repr__")
{
let call = CallArgs { positional: &[], keyword: &IndexMap::new() };
let (returned, _self_after) =
call_method(state, &method, value.clone(), call, tools).await?;
if let Value::String(s) = returned {
return Ok(s.into());
}
}
}
if let Some(class) = state.classes.get(&inst.class_name) {
if let Some(fields) = class.dataclass_fields.clone() {
return Ok(render_dataclass(state, &inst.class_name, inst, &fields));
}
if let Some(Value::Tuple(field_names)) = class.class_attrs.get("_fields") {
let names: Vec<String> = field_names
.iter()
.filter_map(|n| match n {
Value::String(s) => Some(s.to_string()),
_ => None,
})
.collect();
let mut out = format!("{}(", inst.class_name);
for (i, name) in names.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
let field_val =
inst.fields.lock().get(name.as_str()).cloned().unwrap_or(Value::None);
out.push_str(name);
out.push('=');
out.push_str(&render(state, &field_val, RenderMode::Repr, tools).await?);
}
out.push(')');
return Ok(out);
}
}
return Ok(format!("<{} object>", inst.class_name));
}
match value {
Value::List(items) => {
let ptr = std::sync::Arc::as_ptr(items) as usize;
if !state.repr_active.insert(ptr) {
return Ok("[...]".to_string());
}
let snapshot = items.lock().clone();
let result = render_sequence(state, &snapshot, "[", "]", tools).await;
state.repr_active.remove(&ptr);
result
}
Value::Tuple(items) => {
let single = items.len() == 1;
let body = render_sequence(state, items, "(", "", tools).await?;
Ok(if single { format!("{body},)") } else { body + ")" })
}
Value::Set(body) => {
let seq = body.lock().iter_ordered();
if seq.is_empty() {
Ok("set()".to_string())
} else {
Ok(render_sequence(state, &seq, "{", "}", tools).await?)
}
}
Value::Frozenset(body) => {
let seq = body.iter_ordered();
if seq.is_empty() {
Ok("frozenset()".to_string())
} else {
let inner = render_sequence(state, &seq, "{", "}", tools).await?;
Ok(format!("frozenset({inner})"))
}
}
Value::Dict(map) => {
let ptr = std::sync::Arc::as_ptr(map) as usize;
if !state.repr_active.insert(ptr) {
return Ok("{...}".to_string());
}
let snapshot = map.lock().clone();
let result = render_mapping(state, &snapshot, tools).await;
state.repr_active.remove(&ptr);
result
}
Value::Exception(e)
if lookup_method_in_mro(
state,
&e.type_name,
if matches!(mode, RenderMode::Display) { "__str__" } else { "__repr__" },
)
.is_some() =>
{
let slot = if matches!(mode, RenderMode::Display) { "__str__" } else { "__repr__" };
let (_, method) =
lookup_method_in_mro(state, &e.type_name, slot).ok_or_else(|| {
EvalError::from(InterpreterError::Runtime(
"exception render slot vanished after guard".into(),
))
})?;
let call = CallArgs { positional: &[], keyword: &IndexMap::new() };
let (returned, _self) =
call_method(state, &method, value.clone(), call, tools).await?;
match returned {
Value::String(s) => Ok(match mode {
RenderMode::Ascii => ascii_escape(&s),
_ => s.into(),
}),
other => Err(EvalError::from(InterpreterError::TypeError(format!(
"{}.{slot} returned non-string (type {})",
e.type_name,
other.type_name()
)))),
}
}
Value::Exception(e) if matches!(mode, RenderMode::Display) => match e.args.as_slice() {
[] => Ok(String::new()),
[_] if e.type_name == "KeyError" => Ok(e.message.clone()),
[single] => render(state, single, RenderMode::Display, tools).await,
many => Ok(render_sequence(state, many, "(", "", tools).await? + ")"),
},
Value::Class(name) => {
let qualname = state
.classes
.get(name)
.map(|c| if c.qualname.is_empty() { &c.name } else { &c.qualname })
.unwrap_or(name);
Ok(format!("<class '__main__.{qualname}'>"))
}
_ => Ok(match mode {
RenderMode::Repr => value.repr(),
RenderMode::Display => format!("{value}"),
RenderMode::Ascii => ascii_escape(&value.repr()),
}),
}
})
}
async fn render_sequence(
state: &mut InterpreterState,
items: &[Value],
open: &str,
close: &str,
tools: &Tools,
) -> Result<String, EvalError> {
let mut out = String::from(open);
let mut first = true;
for item in items {
if !first {
out.push_str(", ");
}
first = false;
out.push_str(&render(state, item, RenderMode::Repr, tools).await?);
}
out.push_str(close);
Ok(out)
}
async fn render_mapping(
state: &mut InterpreterState,
entries: &IndexMap<crate::value::ValueKey, Value>,
tools: &Tools,
) -> Result<String, EvalError> {
let mut out = String::from("{");
let mut first = true;
for (k, v) in entries {
if !first {
out.push_str(", ");
}
first = false;
out.push_str(&render(state, &k.to_value(), RenderMode::Repr, tools).await?);
out.push_str(": ");
out.push_str(&render(state, v, RenderMode::Repr, tools).await?);
}
out.push('}');
Ok(out)
}
fn render_dataclass(
state: &InterpreterState,
class_name: &str,
inst: &crate::value::InstanceValue,
fields: &[DataclassField],
) -> String {
let mut out = String::new();
out.push_str(class_name);
out.push('(');
let mut first = true;
for field in fields.iter().filter(|f| f.repr && !f.init_only) {
if !first {
out.push_str(", ");
}
first = false;
out.push_str(&field.name);
out.push('=');
let field_value = inst.fields.lock().get(&field.name).cloned().unwrap_or(Value::None);
let rendered = match &field_value {
Value::Instance(nested) => state
.classes
.get(&nested.class_name)
.and_then(|nested_class| nested_class.dataclass_fields.clone())
.map_or_else(
|| field_value.repr(),
|nested_fields| {
render_dataclass(state, &nested.class_name, nested, &nested_fields)
},
),
_ => field_value.repr(),
};
out.push_str(&rendered);
}
out.push(')');
out
}
pub(crate) fn ascii_escape(s: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let cp = c as u32;
if c.is_ascii() {
out.push(c);
} else if cp < 0x100 {
let _ = write!(out, "\\x{cp:02x}");
} else if cp < 0x10000 {
let _ = write!(out, "\\u{cp:04x}");
} else {
let _ = write!(out, "\\U{cp:08x}");
}
}
out
}