1use sim_kernel::{CodecId, Cx, Expr, Result};
10use sim_lib_scene::{SceneBudget, SceneBudgetExhausted, SceneBudgetState, node, sym};
11
12use crate::contract::View;
13
14pub struct UniversalView;
16
17impl View for UniversalView {
18 fn encode(&self, _cx: &mut Cx, value: &Expr) -> Result<Expr> {
19 Ok(node(
20 "stack",
21 vec![
22 ("id", sym("universal")),
23 ("dir", sym("column")),
24 (
25 "children",
26 Expr::List(vec![
27 summary_card(value),
28 structure_tree(value),
29 canonical_text(value),
30 operations_inspector(value),
31 ]),
32 ),
33 ],
34 ))
35 }
36}
37
38fn text_line(text: String) -> Expr {
39 node("text", vec![("text", Expr::String(text))])
40}
41
42fn badge(status: &str, label: &str) -> Expr {
43 node(
45 "badge",
46 vec![
47 ("status", sym(status)),
48 ("label", Expr::String(label.to_owned())),
49 ],
50 )
51}
52
53fn summary_card(value: &Expr) -> Expr {
55 let roundtrip = roundtrip_badge(value);
56 node(
57 "box",
58 vec![
59 ("role", sym("summary")),
60 (
61 "children",
62 Expr::List(vec![
63 text_line(format!("kind: {}", expr_kind(value))),
64 text_line(format!("label: {}", short_label(value))),
65 roundtrip,
66 ]),
67 ),
68 ],
69 )
70}
71
72fn structure_tree(value: &Expr) -> Expr {
74 let mut budget = SceneBudgetState::new(SceneBudget::interactive());
75 node(
76 "box",
77 vec![
78 ("role", sym("structure")),
79 (
80 "children",
81 Expr::List(vec![tree_of("value", value, &mut budget, 0, Vec::new())]),
82 ),
83 ],
84 )
85}
86
87fn tree_of(
88 label: &str,
89 value: &Expr,
90 budget: &mut SceneBudgetState,
91 depth: usize,
92 path: Vec<Expr>,
93) -> Expr {
94 let encoded_bytes = estimated_tree_bytes(label, value);
95 if let Err(exhausted) = budget.admit(depth, Some(label), encoded_bytes) {
96 return continuation_node(label, exhausted);
97 }
98 match value {
99 Expr::Map(entries) => node(
100 "tree",
101 vec![
102 ("label", Expr::String(label.to_owned())),
103 ("open", Expr::Bool(depth == 0)),
104 ("aria-expanded", Expr::Bool(depth == 0)),
105 ("disclosure-target", Expr::List(path.clone())),
106 (
107 "nodes",
108 Expr::List(
109 entries
110 .iter()
111 .map(|(key, child)| {
112 let mut child_path = path.clone();
113 child_path.push(Expr::Vector(vec![sym("k"), key.clone()]));
114 tree_of(&render_value(key), child, budget, depth + 1, child_path)
115 })
116 .collect(),
117 ),
118 ),
119 ],
120 ),
121 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => node(
122 "tree",
123 vec![
124 ("label", Expr::String(format!("{label} [{}]", items.len()))),
125 ("open", Expr::Bool(depth == 0)),
126 ("aria-expanded", Expr::Bool(depth == 0)),
127 ("disclosure-target", Expr::List(path.clone())),
128 (
129 "nodes",
130 Expr::List(
131 items
132 .iter()
133 .enumerate()
134 .map(|(index, child)| {
135 let mut child_path = path.clone();
136 child_path.push(Expr::Vector(vec![
137 sym("i"),
138 Expr::String(index.to_string()),
139 ]));
140 tree_of(&format!("[{index}]"), child, budget, depth + 1, child_path)
141 })
142 .collect(),
143 ),
144 ),
145 ],
146 ),
147 atom => text_line(format!("{label}: {}", render_value(atom))),
148 }
149}
150
151fn continuation_node(label: &str, exhausted: SceneBudgetExhausted) -> Expr {
152 node(
153 "continuation",
154 vec![
155 ("label", Expr::String(format!("{label}: more not rendered"))),
156 ("truncated", Expr::Bool(true)),
157 ("reason", sym(exhausted.reason())),
158 ("limit", Expr::String(exhausted.limit().to_string())),
159 ],
160 )
161}
162
163fn canonical_text(value: &Expr) -> Expr {
170 let mut children = vec![text_line(render_value(value))];
171 match value {
172 Expr::Map(entries) => {
173 for (key, child) in entries {
174 if is_scalar(child) {
175 children.push(editable_leaf(value, key_path(key), child));
176 }
177 }
178 }
179 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
180 for (index, item) in items.iter().enumerate() {
181 if is_scalar(item) {
182 children.push(editable_leaf(value, index_path(index), item));
183 }
184 }
185 }
186 scalar => {
187 children.push(editable_leaf(scalar, Expr::List(Vec::new()), scalar));
191 }
192 }
193 node(
194 "box",
195 vec![
196 ("role", sym("canonical-text")),
197 ("children", Expr::List(children)),
198 ],
199 )
200}
201
202fn is_scalar(value: &Expr) -> bool {
204 !matches!(
205 value,
206 Expr::Map(_) | Expr::List(_) | Expr::Vector(_) | Expr::Set(_)
207 )
208}
209
210fn key_path(key: &Expr) -> Expr {
212 Expr::List(vec![Expr::Vector(vec![sym("k"), key.clone()])])
213}
214
215fn index_path(index: usize) -> Expr {
217 Expr::List(vec![Expr::Vector(vec![
218 sym("i"),
219 Expr::String(index.to_string()),
220 ])])
221}
222
223fn editable_leaf(root: &Expr, path: Expr, leaf: &Expr) -> Expr {
227 let mut fields = vec![
228 ("input-kind", sym("text")),
229 ("value", Expr::String(render_value(leaf))),
230 ("value-kind", sym(expr_kind(leaf))),
231 ("target", root.clone()),
232 ("path", path),
233 ("readonly", Expr::Bool(false)),
234 ];
235 if let Ok(encoded) = sim_codec::encode_portable(CodecId(0), leaf) {
236 fields.push(("value-codec", Expr::String(encoded)));
237 }
238 node("field", fields)
239}
240
241fn operations_inspector(value: &Expr) -> Expr {
243 node(
244 "stack",
245 vec![
246 ("role", sym("operations")),
247 ("dir", sym("column")),
248 (
249 "children",
250 Expr::List(vec![
251 action_button("copy", "Copy", value),
252 action_button("edit", "Edit", value),
253 ]),
254 ),
255 ],
256 )
257}
258
259fn action_button(control: &str, label: &str, value: &Expr) -> Expr {
260 node(
261 "button",
262 vec![
263 ("control", sym(control)),
264 ("label", Expr::String(label.to_owned())),
265 ("target", value.clone()),
266 ],
267 )
268}
269
270fn roundtrip_badge(value: &Expr) -> Expr {
271 if exceeds_depth(value, 64) {
272 return badge("info", "too deep to round-trip inline");
273 }
274 let codec = CodecId(0);
275 match sim_codec::encode_portable(codec, value) {
276 Ok(text) => match sim_codec::decode_portable(codec, &text) {
277 Ok(decoded) if &decoded == value => badge("ok", "round-trips"),
278 Ok(_) => badge("warn", "round-trip differs"),
279 Err(_) => badge("warn", "decode failed"),
280 },
281 Err(_) => badge("info", "non-data value"),
282 }
283}
284
285fn exceeds_depth(value: &Expr, max_depth: usize) -> bool {
286 let mut stack = vec![(value, 0usize)];
287 while let Some((value, depth)) = stack.pop() {
288 if depth > max_depth {
289 return true;
290 }
291 match value {
292 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
293 stack.extend(items.iter().map(|item| (item, depth + 1)));
294 }
295 Expr::Map(entries) => {
296 for (key, value) in entries {
297 stack.push((key, depth + 1));
298 stack.push((value, depth + 1));
299 }
300 }
301 _ => {}
302 }
303 }
304 false
305}
306
307pub(crate) fn universal_regions(value: &Expr) -> Vec<Expr> {
310 vec![
311 summary_card(value),
312 canonical_text(value),
313 structure_tree(value),
314 operations_inspector(value),
315 ]
316}
317
318pub use sim_value::kind::expr_kind;
320
321fn short_label(value: &Expr) -> String {
322 let rendered = render_value(value);
323 if rendered.len() <= 48 {
324 rendered
325 } else {
326 format!("{}...", &rendered[..45])
327 }
328}
329
330pub fn render_value(value: &Expr) -> String {
332 render_value_bounded(value, 0)
333}
334
335fn render_value_bounded(value: &Expr, depth: usize) -> String {
336 if depth >= 64 {
337 return "...".to_owned();
338 }
339 match value {
340 Expr::Nil => "nil".to_owned(),
341 Expr::Bool(flag) => flag.to_string(),
342 Expr::Number(number) => number.canonical.clone(),
343 Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.as_qualified_str(),
344 Expr::String(text) => format!("{text:?}"),
345 Expr::Bytes(bytes) => format!("#bytes({})", bytes.len()),
346 Expr::List(items) => format!("({})", render_items(items, depth + 1)),
347 Expr::Vector(items) => format!("[{}]", render_items(items, depth + 1)),
348 Expr::Set(items) => format!("#{{{}}}", render_items(items, depth + 1)),
349 Expr::Map(entries) => {
350 let body = entries
351 .iter()
352 .take(128)
353 .map(|(key, value)| {
354 format!(
355 "{}: {}",
356 render_value_bounded(key, depth + 1),
357 render_value_bounded(value, depth + 1)
358 )
359 })
360 .collect::<Vec<_>>()
361 .join(", ");
362 if entries.len() > 128 {
363 format!("{{{body}, ...}}")
364 } else {
365 format!("{{{body}}}")
366 }
367 }
368 other => format!("<{}>", expr_kind(other)),
369 }
370}
371
372fn render_items(items: &[Expr], depth: usize) -> String {
373 let rendered = items
374 .iter()
375 .take(128)
376 .map(|item| render_value_bounded(item, depth + 1))
377 .collect::<Vec<_>>()
378 .join(" ");
379 if items.len() > 128 {
380 format!("{rendered} ...")
381 } else {
382 rendered
383 }
384}
385
386fn estimated_tree_bytes(label: &str, value: &Expr) -> usize {
387 let body = match value {
388 Expr::Map(entries) => entries.len().saturating_mul(16),
389 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => items.len().saturating_mul(8),
390 atom => render_value(atom).len(),
391 };
392 label.len().saturating_add(body)
393}