Skip to main content

sim_lib_view_codec/
shape.rs

1//! The Shape lens: matcher visualization, bindings, and counterexamples.
2//!
3//! Shapes are one of SIM's strongest differentiators, so this lens exposes them
4//! directly: it shows what a Shape is (its documentation), whether a value
5//! matches (with the captured bindings), and a failing-match counterexample with
6//! the diagnostics that explain the rejection.
7
8use sim_kernel::{Cx, Expr, Result, Symbol, Value};
9use sim_lib_scene::{node, sym};
10
11/// The Shape lens id.
12pub const SHAPE_LENS: &str = "view:codec-shape";
13
14/// Render a Shape inspected against a matching `value` and a `counterexample`
15/// that should fail, into one Scene.
16pub fn shape_view(
17    cx: &mut Cx,
18    shape_value: &Value,
19    value: &Expr,
20    counterexample: &Expr,
21) -> Result<Expr> {
22    let shape = shape_value
23        .object()
24        .as_shape()
25        .ok_or_else(|| sim_kernel::Error::HostError("value is not a Shape".to_owned()))?;
26
27    let doc = shape.describe(cx)?;
28    let matched = shape.check_expr(cx, value)?;
29    let counter = shape.check_expr(cx, counterexample)?;
30
31    let mut sections = vec![
32        // Matcher description (matcher-tree visualization).
33        matcher_tree(&doc),
34        // The match result for the example value.
35        match_section(
36            "match",
37            value,
38            matched.accepted,
39            &bindings(&matched.captures),
40            &matched.diagnostics,
41        ),
42    ];
43    // The counterexample: it must be rejected; its diagnostics explain why.
44    sections.push(match_section(
45        "counterexample",
46        counterexample,
47        counter.accepted,
48        &[],
49        &counter.diagnostics,
50    ));
51
52    Ok(node(
53        "box",
54        vec![
55            ("role", sym("shape")),
56            (
57                "shape",
58                Expr::Symbol(
59                    shape
60                        .symbol()
61                        .unwrap_or_else(|| Symbol::new(doc.name.clone())),
62                ),
63            ),
64            ("children", Expr::List(sections)),
65        ],
66    ))
67}
68
69fn matcher_tree(doc: &sim_kernel::ShapeDoc) -> Expr {
70    let details = doc
71        .details
72        .iter()
73        .map(|detail| node("text", vec![("text", Expr::String(detail.clone()))]))
74        .collect();
75    node(
76        "tree",
77        vec![
78            ("label", Expr::String(format!("shape: {}", doc.name))),
79            ("nodes", Expr::List(details)),
80        ],
81    )
82}
83
84fn match_section(
85    role: &str,
86    value: &Expr,
87    accepted: bool,
88    bindings: &[String],
89    diagnostics: &[sim_kernel::Diagnostic],
90) -> Expr {
91    let mut children = vec![
92        node(
93            "text",
94            vec![("text", Expr::String(format!("value: {}", render(value))))],
95        ),
96        node(
97            "badge",
98            vec![
99                ("status", sym(if accepted { "ok" } else { "error" })),
100                (
101                    "label",
102                    Expr::String(if accepted { "matches" } else { "rejected" }.to_owned()),
103                ),
104            ],
105        ),
106    ];
107    if !bindings.is_empty() {
108        children.push(node(
109            "text",
110            vec![(
111                "text",
112                Expr::String(format!("bindings: {}", bindings.join(", "))),
113            )],
114        ));
115    }
116    for diagnostic in diagnostics {
117        children.push(node(
118            "text",
119            vec![
120                ("role", sym("diagnostic")),
121                ("text", Expr::String(diagnostic.message.clone())),
122            ],
123        ));
124    }
125    node(
126        "box",
127        vec![("role", sym(role)), ("children", Expr::List(children))],
128    )
129}
130
131fn bindings(captures: &sim_kernel::ShapeBindings) -> Vec<String> {
132    captures
133        .exprs()
134        .iter()
135        .map(|(name, _)| name.to_string())
136        .chain(captures.values().iter().map(|(name, _)| name.to_string()))
137        .collect()
138}
139
140fn render(value: &Expr) -> String {
141    match value {
142        Expr::Symbol(symbol) => symbol.as_qualified_str(),
143        Expr::Number(number) => number.canonical.clone(),
144        Expr::String(text) => format!("{text:?}"),
145        Expr::Bool(flag) => flag.to_string(),
146        Expr::Nil => "nil".to_owned(),
147        other => format!("<{other:?}>"),
148    }
149}