Skip to main content

wolfram_expr/
wl.rs

1//! Rendering of expressions as Wolfram Language source text.
2//!
3//! `Display` (`{}`) produces a compact single line that reads back through
4//! `ToExpression`; `Debug` (`{:?}`) produces the same syntax, indented
5//! recursively. The mode rides along in the `indent: Option<usize>` parameter —
6//! `None` stays on one line, `Some(depth)` breaks nested nodes and indents two
7//! spaces per level. Everything funnels through [`fmt_kind`], the single
8//! renderer, so each variant's textual form and the break/inline rule are
9//! defined exactly once.
10
11use std::fmt;
12use std::sync::Arc;
13
14use crate::{expr, Expr, ExprKind, Normal};
15
16/// Serialize `expr` to WXF bytes and format as `BinaryDeserialize[ByteArray["<base64>"]]`.
17/// Built with `expr!` and rendered through `fmt_kind` so the bracketing and
18/// string escaping come from the same place as everything else.
19fn wxf_display(
20    f: &mut fmt::Formatter,
21    expr: &Expr,
22    indent: Option<usize>,
23) -> fmt::Result {
24    use base64::Engine;
25    match wolfram_serialize::to_wxf(expr, None) {
26        Ok(bytes) => {
27            let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
28            let e = expr!(::BinaryDeserialize[::ByteArray[(b64)]]);
29            fmt_kind(f, e.kind(), indent)
30        },
31        Err(_) => write!(f, "Failure[\"BinarySerializeError\"]"),
32    }
33}
34
35/// True for the structural variants (`Normal`, `Association`) — the ones the
36/// pretty-printer may break across lines. Everything else is an atom.
37fn is_compound(kind: &ExprKind) -> bool {
38    matches!(kind, ExprKind::Normal(_) | ExprKind::Association(_))
39}
40
41/// A compound that itself contains a compound — i.e. it nests two or more
42/// levels deep. A node breaks across lines (when indenting) only when one of
43/// its children is nested; a child that is an atom or a shallow compound like
44/// `Slot[1]` or `List[a, b]` stays inline.
45fn is_nested(kind: &ExprKind) -> bool {
46    match kind {
47        ExprKind::Normal(n) => n.contents.iter().any(|e| is_compound(e.kind())),
48        ExprKind::Association(a) => a.iter().any(|e| is_compound(e.value.kind())),
49        _ => false,
50    }
51}
52
53/// The child indent for a node rendered at `indent`: `Some(d + 1)` when it
54/// breaks (only possible when indenting and a child is nested), else `indent`
55/// unchanged.
56fn child_indent(indent: Option<usize>, breaks: bool) -> Option<usize> {
57    if breaks {
58        indent.map(|d| d + 1)
59    } else {
60        indent
61    }
62}
63
64/// Write a `len`-item sequence between `open`/`close`. When `brk`, each item
65/// goes on its own line indented to `depth + 1` with the close back at `depth`
66/// (`depth` taken from `indent`); otherwise it's one line, items separated by
67/// `, `. `item(f, i)` renders the `i`-th item.
68fn fmt_seq<F>(
69    f: &mut fmt::Formatter,
70    indent: Option<usize>,
71    open: &str,
72    close: &str,
73    len: usize,
74    brk: bool,
75    mut item: F,
76) -> fmt::Result
77where
78    F: FnMut(&mut fmt::Formatter, usize) -> fmt::Result,
79{
80    let depth = indent.unwrap_or(0);
81    f.write_str(open)?;
82    for i in 0..len {
83        if brk {
84            write!(f, "\n{}", "  ".repeat(depth + 1))?;
85        } else if i > 0 {
86            f.write_str(", ")?;
87        }
88        item(f, i)?;
89        if brk && i + 1 < len {
90            f.write_str(",")?;
91        }
92    }
93    if brk {
94        write!(f, "\n{}", "  ".repeat(depth))?;
95    }
96    f.write_str(close)
97}
98
99/// Render a `Normal` by dispatching on its head: a `System``-qualified or
100/// context-less symbol with a known WL surface syntax gets it (`List` → `{…}`,
101/// `Rule`/`RuleDelayed`/`Set` → infix, `Slot`/`SlotSequence` → `#`/`##`);
102/// anything else renders as `head[…]`. Shared by `fmt_kind` and `Display for
103/// Normal` so neither needs to wrap/clone the other.
104fn fmt_normal(f: &mut fmt::Formatter, n: &Normal, indent: Option<usize>) -> fmt::Result {
105    let ExprKind::Symbol(sym) = n.head.kind() else {
106        return fmt_call(f, n, indent);
107    };
108    match sym.as_str() {
109        "System`List" | "List" => fmt_list(f, n, indent),
110        "System`Rule" | "Rule" => fmt_infix(f, n, indent, "->"),
111        "System`RuleDelayed" | "RuleDelayed" => fmt_infix(f, n, indent, ":>"),
112        "System`Set" | "Set" => fmt_infix(f, n, indent, "="),
113        "System`Slot" | "Slot" => fmt_slot(f, n, indent, "#"),
114        "System`SlotSequence" | "SlotSequence" => fmt_slot(f, n, indent, "##"),
115        _ => fmt_call(f, n, indent),
116    }
117}
118
119/// `open … item, item … close`, breaking (when indenting) if a child is nested.
120fn fmt_delimited(
121    f: &mut fmt::Formatter,
122    n: &Normal,
123    indent: Option<usize>,
124    open: &str,
125    close: &str,
126) -> fmt::Result {
127    let brk = indent.is_some() && n.contents.iter().any(|e| is_nested(e.kind()));
128    let inner = child_indent(indent, brk);
129    fmt_seq(f, indent, open, close, n.contents.len(), brk, |f, i| {
130        fmt_kind(f, n.contents[i].kind(), inner)
131    })
132}
133
134/// Default: `head[a, b, …]`.
135fn fmt_call(f: &mut fmt::Formatter, n: &Normal, indent: Option<usize>) -> fmt::Result {
136    fmt_delimited(f, n, indent, &format!("{}[", n.head), "]")
137}
138
139/// `List[…]` → `{…}`.
140fn fmt_list(f: &mut fmt::Formatter, n: &Normal, indent: Option<usize>) -> fmt::Result {
141    fmt_delimited(f, n, indent, "{", "}")
142}
143
144/// Binary infix `a op b`. Only a 2-argument head is infix; any other arity
145/// falls back to `head[…]`.
146fn fmt_infix(
147    f: &mut fmt::Formatter,
148    n: &Normal,
149    indent: Option<usize>,
150    op: &str,
151) -> fmt::Result {
152    if n.contents.len() != 2 {
153        return fmt_call(f, n, indent);
154    }
155    fmt_kind(f, n.contents[0].kind(), indent)?;
156    write!(f, " {op} ")?;
157    fmt_kind(f, n.contents[1].kind(), indent)
158}
159
160/// `prefix` immediately followed by a single positional (`Slot[1]` → `#1`) or
161/// named (`Slot["foo"]` → `#foo`, not `#"foo"`) argument. Anything else — a
162/// non-`Integer`/`String` argument, or any arity but one — falls back to
163/// `head[…]`.
164fn fmt_slot(
165    f: &mut fmt::Formatter,
166    n: &Normal,
167    indent: Option<usize>,
168    prefix: &str,
169) -> fmt::Result {
170    match n.contents.as_slice() {
171        [arg] => match arg.kind() {
172            ExprKind::String(name) => {
173                f.write_str(prefix)?;
174                f.write_str(name)
175            },
176            kind @ ExprKind::Integer(_) => {
177                f.write_str(prefix)?;
178                fmt_kind(f, kind, indent)
179            },
180            _ => fmt_call(f, n, indent),
181        },
182        _ => fmt_call(f, n, indent),
183    }
184}
185
186/// The single renderer for every [`ExprKind`]. `indent` is `None` for the
187/// compact (`Display`) form and `Some(depth)` for the indented (`Debug`) form,
188/// which breaks `Normal`/`Association` nodes that contain a nested child. The
189/// per-variant formatting — how each leaf prints — is defined here, once.
190fn fmt_kind(
191    f: &mut fmt::Formatter,
192    kind: &ExprKind,
193    indent: Option<usize>,
194) -> fmt::Result {
195    match kind {
196        ExprKind::Normal(n) => fmt_normal(f, n, indent),
197        ExprKind::Association(a) => {
198            let brk = indent.is_some() && a.iter().any(|e| is_nested(e.value.kind()));
199            let inner = child_indent(indent, brk);
200            fmt_seq(f, indent, "<|", "|>", a.len(), brk, |f, i| {
201                let entry = &a[i];
202                let arrow = if entry.delayed { ":>" } else { "->" };
203                write!(f, "{} {arrow} ", entry.key)?;
204                fmt_kind(f, entry.value.kind(), inner)
205            })
206        },
207        ExprKind::Integer(int) => write!(f, "{int}"),
208        // The float's Debug form keeps a decimal point (`1.0`, not `1`);
209        // NotNan's surprising Display would drop it.
210        ExprKind::Real(real) => write!(f, "{:?}", **real),
211        // Escape via Debug so the result reads back through `ToExpression`
212        // (`\n`, `\t`, `"` etc. become their escape sequences).
213        ExprKind::String(string) => write!(f, "{string:?}"),
214        ExprKind::Symbol(symbol) => write!(f, "{symbol}"),
215        ExprKind::ByteArray(ba) => {
216            use base64::Engine;
217            let b64 = base64::engine::general_purpose::STANDARD.encode(ba.as_slice());
218            fmt_kind(f, expr!(::ByteArray[(b64)]).kind(), indent)
219        },
220        ExprKind::NumericArray(arr) => {
221            let expr = Expr {
222                inner: Arc::new(ExprKind::NumericArray(arr.clone())),
223            };
224            wxf_display(f, &expr, indent)
225        },
226        ExprKind::PackedArray(arr) => {
227            let expr = Expr {
228                inner: Arc::new(ExprKind::PackedArray(arr.clone())),
229            };
230            wxf_display(f, &expr, indent)
231        },
232        ExprKind::BigInteger(n) => write!(f, "{}", n.as_str()),
233        ExprKind::BigReal(r) => write!(f, "{}", r.as_str()),
234    }
235}
236
237impl fmt::Display for Expr {
238    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
239        fmt_kind(f, self.kind(), None)
240    }
241}
242
243impl fmt::Display for ExprKind {
244    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
245        fmt_kind(f, self, None)
246    }
247}
248
249impl fmt::Debug for ExprKind {
250    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251        fmt_kind(f, self, Some(0))
252    }
253}
254
255impl fmt::Display for Normal {
256    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
257        fmt_normal(f, self, None)
258    }
259}
260
261#[allow(deprecated)]
262impl fmt::Display for crate::Number {
263    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
264        match self {
265            crate::Number::Integer(i) => fmt_kind(f, &ExprKind::Integer(*i), None),
266            crate::Number::Real(r) => fmt_kind(f, &ExprKind::Real(*r), None),
267        }
268    }
269}