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