Skip to main content

arena_terms/
display.rs

1//! Defines [`TermDisplay`], a formatter for rendering [`Term`] values.
2//!
3//! Provides a [`fmt::Display`] implementation for human-readable output of
4//! terms, including atoms, lists, tuples, and compound structures.
5
6use crate::{Arena, Term, View};
7use std::fmt;
8
9/// A wrapper that ties together a [`Term`] and its [`Arena`], forming the
10/// basis for configurable pretty-printing. This type is designed as the
11/// foundation on which flexible formatting and printing of terms will be built.
12///
13/// It already implements [`fmt::Display`], so you can seamlessly use it with
14/// standard formatting macros (`format!`, `println!`, etc.) to render
15/// terms. In the future, it will also support additional, customizable
16/// formatting options for advanced pretty-printing.
17///
18/// ### Example
19/// ```rust
20/// use arena_terms::{Term, Arena, func, IntoTerm};
21/// let mut arena = Arena::new();
22/// let term = func!("foo"; 1, "hello, world!" => &mut arena);
23///
24/// println!("{}", term.display(&arena));
25/// ```
26///
27/// Construct instances via [`Term::display`] or [`Arena::display`].
28pub struct TermDisplay<'a> {
29    /// The interned term to display.
30    term: &'a Term,
31    /// The arena where the term is stored.
32    arena: &'a Arena,
33}
34
35impl Term {
36    /// Return a [`TermDisplay`] suitable for formatting with [`fmt::Display`].
37    ///
38    /// Use this method when you want to render a term:
39    ///
40    /// ```ignore
41    /// println!("{}", term.display(&arena));
42    /// ```
43    #[inline]
44    pub fn display<'a>(&'a self, arena: &'a Arena) -> TermDisplay<'a> {
45        TermDisplay { term: self, arena }
46    }
47}
48
49/// Implements [`fmt::Display`] for [`TermDisplay`], enabling it to be
50/// formatted and printed with standard formatting macros.
51impl<'a> fmt::Display for TermDisplay<'a> {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        fn is_unquoted_atom(s: &str) -> bool {
54            let mut chars = s.chars();
55            match chars.next() {
56                Some(c) if c.is_ascii_lowercase() => {}
57                _ => return false,
58            }
59            chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
60        }
61
62        fn write_atom(f: &mut fmt::Formatter<'_>, arena: &Arena, functor: &Term) -> fmt::Result {
63            let s = arena.atom_name(functor).map_err(|_e| fmt::Error)?;
64            write_atom_str(f, s)
65        }
66
67        fn write_atom_str(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
68            if s.is_empty() || !is_unquoted_atom(s) {
69                let escaped = s.replace('\'', "\\'");
70                write!(f, "'{}'", escaped)
71            } else {
72                write!(f, "{}", s)
73            }
74        }
75
76        fn write_str_quoted(f: &mut fmt::Formatter<'_>, s: &str) -> fmt::Result {
77            let mut out = String::new();
78            out.push('"');
79            for ch in s.chars() {
80                match ch {
81                    '\\' => out.push_str("\\\\"),
82                    '"' => out.push_str("\\\""),
83                    '{' => out.push_str("\\{"),
84                    '}' => out.push_str("\\}"),
85                    '\n' => out.push_str("\\n"),
86                    '\r' => out.push_str("\\r"),
87                    '\t' => out.push_str("\\t"),
88                    '\x07' => out.push_str("\\a"),
89                    '\x08' => out.push_str("\\b"),
90                    '\x0C' => out.push_str("\\f"),
91                    '\x0B' => out.push_str("\\v"),
92                    '\x1B' => out.push_str("\\e"),
93                    '\x7F' => out.push_str("\\d"),
94                    c if c.is_control() => out.push_str(&format!("\\x{:02X}", c as u32)),
95                    c => out.push(c),
96                }
97            }
98            out.push('"');
99            f.write_str(&out)
100        }
101
102        fn epoch_to_date_string(epoch_ms: i64, fmt: Option<&str>) -> String {
103            use chrono::{DateTime, Utc};
104
105            let secs = epoch_ms.div_euclid(1000);
106            let nsecs = (epoch_ms.rem_euclid(1000) * 1_000_000) as u32;
107
108            let dt_utc = DateTime::<Utc>::from_timestamp(secs, nsecs).unwrap();
109
110            String::from(match fmt {
111                None => dt_utc.to_rfc3339(),
112                Some(layout) => dt_utc.format(layout).to_string(),
113            })
114        }
115
116        fn write_args(f: &mut fmt::Formatter<'_>, arena: &Arena, args: &[Term]) -> fmt::Result {
117            for (i, t) in args.iter().enumerate() {
118                if i > 0 {
119                    f.write_str(", ")?;
120                }
121                write!(f, "{}", t.display(arena))?;
122            }
123            Ok(())
124        }
125
126        match self.term.view(self.arena).map_err(|_e| fmt::Error)? {
127            View::Int(i) => write!(f, "{i}"),
128            View::Real(r) => {
129                if r.fract() == 0.0 {
130                    write!(f, "{:.1}", r)
131                } else {
132                    write!(f, "{}", r)
133                }
134            }
135            View::Date(epoch) => write!(f, "date{{{}}}", epoch_to_date_string(epoch, None)),
136            View::Str(s) => write_str_quoted(f, s),
137            View::Bin(bytes) => {
138                write!(f, "hex{{")?;
139                for b in bytes {
140                    write!(f, "{:02X}", b)?;
141                }
142                write!(f, "}}")
143            }
144            View::Atom(a) => write_atom_str(f, a),
145            View::Var(v) => write!(f, "{}", v),
146
147            View::Func(ar, functor, args) => {
148                if args.is_empty() {
149                    return write!(f, "/* invalid Func */");
150                }
151                write_atom(f, ar, functor)?;
152                write!(f, "(")?;
153                write_args(f, ar, args)?;
154                write!(f, ")")
155            }
156
157            View::Tuple(ar, items) => {
158                if items.is_empty() {
159                    write!(f, "()")
160                } else {
161                    write!(f, "(")?;
162                    write_args(f, ar, items)?;
163                    write!(f, ")")
164                }
165            }
166
167            View::List(ar, items, tail) => {
168                if items.is_empty() {
169                    write!(f, "[]")
170                } else {
171                    write!(f, "[")?;
172                    write_args(f, ar, items)?;
173                    if *tail != Term::NIL {
174                        f.write_str(" | ")?;
175                        write!(f, "{}", tail.display(ar))?;
176                    }
177                    write!(f, "]")
178                }
179            }
180        }
181    }
182}