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));
}
}
return Ok(format!("<{} object>", inst.class_name));
}
match value {
Value::List(items) => {
let snapshot = items.lock().clone();
Ok(render_sequence(state, &snapshot, "[", "]", tools).await?)
}
Value::Tuple(items) => {
let single = items.len() == 1;
let body = render_sequence(state, items, "(", "", tools).await?;
Ok(if single { format!("{},)", body.trim_end_matches(')')) } else { body + ")" })
}
Value::Set(items) => {
if items.is_empty() {
Ok("set()".to_string())
} else {
Ok(render_sequence(state, items, "{", "}", tools).await?)
}
}
Value::Dict(map) => {
let mut out = String::from("{");
let mut first = true;
for (k, v) in map {
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)
}
_ => 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)
}
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) {
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
}
fn ascii_escape(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii() { c.to_string() } else { format!("\\u{:04x}", c as u32) })
.collect()
}