Skip to main content

sui_eval/
render.rs

1//! The tree-walker's normalized differential render.
2//!
3//! Renders an evaluated [`Value`](crate::Value) to the ONE normalized textual
4//! form the sui↔sui differential (`sui-ir/tests/eval_differential.rs`) and the
5//! `SUI_IR` shadow-eval latch (`sui eval`) byte-compare against `eval_ir`'s
6//! `sui_ir::render::render_ir_value`: CppNix float format, sorted attrs, the
7//! walker's string escaping, raw (unquoted) paths, and a shared depth cap so an
8//! infinitely-deep value renders byte-identically on both engines (a match,
9//! never a stack overflow). Deep-forcing + error-propagating (unlike `Display`,
10//! which swallows a failed force — the differential must see errors as errors).
11//!
12//! Promoted from the test-only `sui-ir/tests/common/render.rs` into shippable
13//! code so the walker's authoritative result can be compared to a shadow
14//! `eval_ir` result live. The cap + escaping MUST match
15//! `sui_ir::render::{MAX_RENDER_DEPTH, DEEP_SENTINEL, escape_str}`.
16
17use crate::value::Concrete;
18use crate::Value;
19
20/// Render-recursion depth cap — must equal `sui_ir::render::MAX_RENDER_DEPTH`.
21pub const MAX_RENDER_DEPTH: usize = 128;
22
23/// The identical marker both engines emit past [`MAX_RENDER_DEPTH`].
24pub const DEEP_SENTINEL: &str = "<...>";
25
26/// The walker's `Display` string escaping.
27#[must_use]
28pub fn escape_str(s: &str) -> String {
29    s.replace('\\', "\\\\").replace('"', "\\\"")
30}
31
32/// Render a tree-walker [`Value`] to the normalized differential form.
33///
34/// # Errors
35///
36/// Propagates any force error as its `Display` string (the differential must
37/// see errors as errors, not swallow them into `<<thunk:error>>`).
38pub fn render_tree(v: &Value) -> Result<String, String> {
39    render_tree_at(v, 0)
40}
41
42fn render_tree_at(v: &Value, depth: usize) -> Result<String, String> {
43    if depth >= MAX_RENDER_DEPTH {
44        return Ok(DEEP_SENTINEL.to_string());
45    }
46    let c = crate::eval::force_concrete(v).map_err(|e| e.to_string())?;
47    Ok(match c {
48        Concrete::Null => "null".to_string(),
49        Concrete::Bool(b) => b.to_string(),
50        Concrete::Int(n) => n.to_string(),
51        Concrete::Float(f) => sui_compat::versions::cppnix_format_float(f),
52        Concrete::String(s) => {
53            let mut out = String::from("\"");
54            out.push_str(&escape_str(&s));
55            out.push('"');
56            out
57        }
58        Concrete::Path(p) => p.to_string(),
59        Concrete::List(items) => {
60            let mut out = String::from("[ ");
61            for item in items.iter() {
62                out.push_str(&render_tree_at(item, depth + 1)?);
63                out.push(' ');
64            }
65            out.push(']');
66            out
67        }
68        Concrete::Attrs(attrs) => {
69            let mut out = String::from("{ ");
70            for (k, v) in attrs.iter() {
71                out.push_str(&k);
72                out.push_str(" = ");
73                out.push_str(&render_tree_at(v, depth + 1)?);
74                out.push_str("; ");
75            }
76            out.push('}');
77            out
78        }
79        Concrete::Lambda(_) => "<<lambda>>".to_string(),
80        Concrete::Builtin(b) => {
81            let mut out = String::from("<<builtin ");
82            out.push_str(b.name);
83            out.push_str(">>");
84            out
85        }
86    })
87}