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}