use std::collections::BTreeMap;
use crate::intern::Interner;
use crate::value::{ThunkState, VMValue};
pub const MAX_RENDER_DEPTH: usize = 128;
pub const DEEP_SENTINEL: &str = "<...>";
#[must_use]
pub fn escape_str(s: &str) -> String {
s.replace('\\', "\\\\").replace('"', "\\\"")
}
pub fn render_vm(v: &VMValue, interner: &Interner) -> Result<String, String> {
render_at(v, interner, 0)
}
fn render_at(v: &VMValue, interner: &Interner, depth: usize) -> Result<String, String> {
if depth >= MAX_RENDER_DEPTH {
return Ok(DEEP_SENTINEL.to_string());
}
Ok(match v {
VMValue::Null => "null".to_string(),
VMValue::Bool(b) => b.to_string(),
VMValue::Int(n) => n.to_string(),
VMValue::Float(f) => sui_compat::versions::cppnix_format_float(*f),
VMValue::String(s) => {
let mut out = String::from("\"");
out.push_str(&escape_str(s));
out.push('"');
out
}
VMValue::Path(p) => p.clone(),
VMValue::List(items) => {
let mut out = String::from("[ ");
for item in items {
out.push_str(&render_at(item, interner, depth + 1)?);
out.push(' ');
}
out.push(']');
out
}
VMValue::Attrs(attrs) => {
let mut by_name: BTreeMap<String, &VMValue> = BTreeMap::new();
for (sym, val) in attrs {
by_name.insert(interner.resolve(*sym).to_string(), val);
}
if by_name.len() != attrs.len() {
return Err(format!(
"interner resolved {} symbols to {} distinct names — an \
attribute would be silently dropped by the name sort",
attrs.len(),
by_name.len()
));
}
let mut out = String::from("{ ");
for (k, val) in by_name {
out.push_str(&k);
out.push_str(" = ");
out.push_str(&render_at(val, interner, depth + 1)?);
out.push_str("; ");
}
out.push('}');
out
}
VMValue::Closure(_) => "<<lambda>>".to_string(),
VMValue::Builtin(b) => {
let mut out = String::from("<<builtin ");
out.push_str(b.name);
out.push_str(">>");
out
}
VMValue::HigherOrderBuiltin(h) => format!("<<builtin {:?}>>", h.op),
VMValue::Thunk(t) => {
let state = t.state.take();
let done = match &state {
Some(ThunkState::Done(inner)) => Some(inner.clone()),
_ => None,
};
t.state.set(state);
match done {
Some(inner) => render_at(&inner, interner, depth + 1)?,
None => {
return Err(
"unforced thunk survived VM deep-force — refusing to render a \
placeholder, because a placeholder compares EQUAL to the other \
engine's placeholder and reports agreement where neither engine \
produced a value"
.to_string(),
);
}
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::VMThunk;
fn interner() -> Interner {
Interner::new()
}
#[test]
fn scalars_match_the_walkers_forms() {
let i = interner();
assert_eq!(render_vm(&VMValue::Null, &i).unwrap(), "null");
assert_eq!(render_vm(&VMValue::Bool(true), &i).unwrap(), "true");
assert_eq!(render_vm(&VMValue::Int(-3), &i).unwrap(), "-3");
assert_eq!(
render_vm(&VMValue::String("a\"b\\c".into()), &i).unwrap(),
"\"a\\\"b\\\\c\""
);
assert_eq!(render_vm(&VMValue::Path("/x/y".into()), &i).unwrap(), "/x/y");
}
#[test]
fn attrs_sort_by_resolved_name_not_symbol_id() {
let mut i = interner();
let z = i.intern("zzz");
let a = i.intern("aaa");
let mut attrs = BTreeMap::new();
attrs.insert(z, VMValue::Int(1));
attrs.insert(a, VMValue::Int(2));
assert_eq!(
render_vm(&VMValue::Attrs(attrs), &i).unwrap(),
"{ aaa = 2; zzz = 1; }"
);
}
#[test]
fn an_unforced_thunk_is_an_error_not_a_placeholder() {
let i = interner();
let t = VMThunk::new(std::rc::Rc::new(crate::chunk::Chunk::new()), Vec::new());
let err = render_vm(&VMValue::Thunk(t), &i).unwrap_err();
assert!(
err.contains("unforced thunk"),
"expected a refusal, got: {err}"
);
}
#[test]
fn a_forced_thunk_renders_its_value() {
let i = interner();
let t = VMThunk::new_done(VMValue::Int(7));
assert_eq!(render_vm(&VMValue::Thunk(t), &i).unwrap(), "7");
}
#[test]
fn the_depth_cap_matches_the_other_engines() {
assert_eq!(MAX_RENDER_DEPTH, sui_eval_render_depth_pin());
assert_eq!(DEEP_SENTINEL, "<...>");
}
fn sui_eval_render_depth_pin() -> usize {
128
}
}