Skip to main content

pretty_lang/
doc.rs

1//! The [`Doc`] document algebra: a small set of combinators for describing how
2//! source text should be laid out, independent of any concrete width.
3//!
4//! A [`Doc`] is a lazy description, not a string. You build it from an AST with
5//! [`text`](Doc::text), [`line`](Doc::line), [`nest`](Doc::nest),
6//! [`group`](Doc::group), and [`append`](Doc::append); the concrete layout is
7//! decided later by [`render`](Doc::render) against a target width. The design
8//! follows Wadler's *A Prettier Printer* and Lindig's *Strictly Pretty*.
9
10use alloc::borrow::Cow;
11use alloc::rc::Rc;
12use alloc::string::String;
13use alloc::vec::Vec;
14
15/// An immutable, cheaply-clonable description of a document's layout.
16///
17/// A `Doc` records *intent* — "these pieces belong together", "break here if the
18/// line is too long", "indent the inside by four" — and leaves the choice of
19/// concrete line breaks to [`render`](Doc::render), which fits the document to a
20/// target width. The same `Doc` renders differently at width 40 and width 120
21/// with no change to how it was built.
22///
23/// # Cloning
24///
25/// `Doc` is a thin handle around a reference-counted node ([`Rc`]), so
26/// [`Clone`] is a pointer-count bump, not a deep copy. Sharing a sub-document in
27/// several places costs one `Rc` clone each. `Doc` is single-threaded by design
28/// (it is not `Send`/`Sync`); a formatter builds and renders on one thread.
29///
30/// # Examples
31///
32/// ```
33/// use pretty_lang::Doc;
34///
35/// // `[1, 2, 3]` — flat because it fits.
36/// let list = Doc::text("[")
37///     .append(Doc::join(
38///         Doc::text(",").append(Doc::line()),
39///         ["1", "2", "3"].map(Doc::text),
40///     ))
41///     .append(Doc::text("]"))
42///     .group();
43///
44/// assert_eq!(list.render(80), "[1, 2, 3]");
45/// ```
46#[derive(Clone)]
47pub struct Doc(pub(crate) Rc<Node>);
48
49/// The internal node kinds behind a [`Doc`]. Kept `pub(crate)`: the public
50/// surface is the combinator API on [`Doc`], never the node shape.
51pub(crate) enum Node {
52    /// The empty document. Renders to nothing.
53    Nil,
54    /// Literal text with its precomputed display width (in Unicode scalars).
55    /// The text MUST NOT contain a newline; use [`Doc::hardline`] for those.
56    Text(Cow<'static, str>, usize),
57    /// A space when the enclosing group is flat, a newline when it is broken.
58    Line,
59    /// Nothing when the enclosing group is flat, a newline when it is broken.
60    SoftLine,
61    /// Always a newline; forces every enclosing group to break.
62    HardLine,
63    /// Concatenation of two documents, laid out left then right.
64    Cat(Doc, Doc),
65    /// Adds `isize` columns of indentation to line breaks inside `Doc`.
66    Nest(isize, Doc),
67    /// A layout choice point: render the inside flat if it fits the remaining
68    /// width on the current line, otherwise break every flexible line in it.
69    Group(Doc),
70}
71
72impl Doc {
73    /// The empty document. It renders to nothing and is the identity for
74    /// [`append`](Doc::append).
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use pretty_lang::Doc;
80    ///
81    /// assert_eq!(Doc::nil().render(80), "");
82    /// assert_eq!(Doc::text("x").append(Doc::nil()).render(80), "x");
83    /// ```
84    #[inline]
85    #[must_use]
86    pub fn nil() -> Doc {
87        Doc(Rc::new(Node::Nil))
88    }
89
90    /// A literal piece of text.
91    ///
92    /// The argument is anything that converts into a `Cow<'static, str>`, so a
93    /// string literal is stored without allocating and an owned `String` is
94    /// moved in. The display width is measured once, here, as the number of
95    /// Unicode scalar values.
96    ///
97    /// # Panics
98    ///
99    /// Never panics. The text is treated as a single unbreakable unit; it MUST
100    /// NOT contain a `'\n'` (embed line breaks with [`line`](Doc::line),
101    /// [`softline`](Doc::softline), or [`hardline`](Doc::hardline) so the layout
102    /// engine can account for them). A newline inside `text` is rendered
103    /// verbatim but throws the width accounting off.
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// use pretty_lang::Doc;
109    ///
110    /// // A static literal — no allocation.
111    /// assert_eq!(Doc::text("let x").render(80), "let x");
112    ///
113    /// // An owned, computed string.
114    /// let name = format!("v{}", 42);
115    /// assert_eq!(Doc::text(name).render(80), "v42");
116    /// ```
117    #[inline]
118    #[must_use]
119    pub fn text(s: impl Into<Cow<'static, str>>) -> Doc {
120        let s = s.into();
121        let width = s.chars().count();
122        Doc(Rc::new(Node::Text(s, width)))
123    }
124
125    /// A flexible break that is a single space when its group is laid out flat
126    /// and a newline (plus the current indentation) when the group breaks.
127    ///
128    /// This is the workhorse separator: put it between items that should sit on
129    /// one line when they fit and stack one-per-line when they do not.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use pretty_lang::Doc;
135    ///
136    /// let doc = Doc::text("a").append(Doc::line()).append(Doc::text("b")).group();
137    /// assert_eq!(doc.render(80), "a b");   // fits: space
138    /// assert_eq!(doc.render(1), "a\nb");   // too narrow: newline
139    /// ```
140    #[inline]
141    #[must_use]
142    pub fn line() -> Doc {
143        Doc(Rc::new(Node::Line))
144    }
145
146    /// A flexible break that is *nothing* when its group is flat and a newline
147    /// (plus indentation) when the group breaks. Use it where a broken layout
148    /// wants a line break but a flat layout wants no space at all — for example
149    /// right after an opening bracket.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use pretty_lang::Doc;
155    ///
156    /// let doc = Doc::text("(")
157    ///     .append(Doc::softline())
158    ///     .append(Doc::text("x"))
159    ///     .group();
160    /// assert_eq!(doc.render(80), "(x");  // flat: no gap
161    /// ```
162    #[inline]
163    #[must_use]
164    pub fn softline() -> Doc {
165        Doc(Rc::new(Node::SoftLine))
166    }
167
168    /// A break that is *always* a newline, and forces every group that contains
169    /// it to break. Use it for constructs that must never be collapsed onto one
170    /// line, such as line comments or statement separators in block bodies.
171    ///
172    /// # Examples
173    ///
174    /// ```
175    /// use pretty_lang::Doc;
176    ///
177    /// let doc = Doc::text("a").append(Doc::hardline()).append(Doc::text("b")).group();
178    /// // Even though "a b" would fit at width 80, the hardline forces a break.
179    /// assert_eq!(doc.render(80), "a\nb");
180    /// ```
181    #[inline]
182    #[must_use]
183    pub fn hardline() -> Doc {
184        Doc(Rc::new(Node::HardLine))
185    }
186
187    /// Concatenate `self` with `other`, laid out left then right. This is the
188    /// fundamental way to build a document up from parts.
189    ///
190    /// [`nil`](Doc::nil) is the identity: `a.append(Doc::nil())` and
191    /// `Doc::nil().append(a)` both render exactly as `a`.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use pretty_lang::Doc;
197    ///
198    /// let doc = Doc::text("fn ").append(Doc::text("main")).append(Doc::text("()"));
199    /// assert_eq!(doc.render(80), "fn main()");
200    /// ```
201    #[inline]
202    #[must_use]
203    pub fn append(self, other: Doc) -> Doc {
204        Doc(Rc::new(Node::Cat(self, other)))
205    }
206
207    /// Increase the indentation applied to every line break *inside* `self` by
208    /// `indent` columns. Indentation is relative and nests: an inner `nest(4)`
209    /// inside an outer `nest(4)` indents broken lines by eight.
210    ///
211    /// `indent` is an `isize`; a negative value dedents. The effective
212    /// indentation never goes below zero (it is clamped at the point a newline
213    /// is emitted).
214    ///
215    /// Only line breaks that actually happen are affected — `nest` on a document
216    /// that stays flat has no visible effect.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use pretty_lang::Doc;
222    ///
223    /// let body = Doc::text("{")
224    ///     .append(
225    ///         Doc::line()
226    ///             .append(Doc::text("stmt;"))
227    ///             .nest(4),
228    ///     )
229    ///     .append(Doc::line())
230    ///     .append(Doc::text("}"))
231    ///     .group();
232    ///
233    /// assert_eq!(body.render(4), "{\n    stmt;\n}");
234    /// ```
235    #[inline]
236    #[must_use]
237    pub fn nest(self, indent: isize) -> Doc {
238        Doc(Rc::new(Node::Nest(indent, self)))
239    }
240
241    /// Mark `self` as a layout choice point.
242    ///
243    /// When the renderer reaches a group it first asks whether the group's
244    /// contents fit, laid out flat, in the width remaining on the current line.
245    /// If they do, every flexible break inside becomes its flat form (a space or
246    /// nothing). If they do not — or the group contains a
247    /// [`hardline`](Doc::hardline) — every flexible break inside becomes a
248    /// newline. The decision is all-or-nothing for the breaks *directly* owned
249    /// by this group; nested groups are decided independently.
250    ///
251    /// Grouping is what turns one document into "one line if it fits, otherwise
252    /// stacked". A document with no groups always uses the broken form of every
253    /// break.
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use pretty_lang::Doc;
259    ///
260    /// let call = Doc::text("f(")
261    ///     .append(
262    ///         Doc::softline()
263    ///             .append(Doc::join(
264    ///                 Doc::text(",").append(Doc::line()),
265    ///                 ["alpha", "beta", "gamma"].map(Doc::text),
266    ///             ))
267    ///             .nest(4),
268    ///     )
269    ///     .append(Doc::softline())
270    ///     .append(Doc::text(")"))
271    ///     .group();
272    ///
273    /// assert_eq!(call.render(80), "f(alpha, beta, gamma)");
274    /// assert_eq!(
275    ///     call.render(10),
276    ///     "f(\n    alpha,\n    beta,\n    gamma\n)"
277    /// );
278    /// ```
279    #[inline]
280    #[must_use]
281    pub fn group(self) -> Doc {
282        Doc(Rc::new(Node::Group(self)))
283    }
284
285    /// Concatenate every document produced by `docs`, in order. Returns
286    /// [`nil`](Doc::nil) for an empty iterator.
287    ///
288    /// This is a left fold of [`append`](Doc::append) and allocates one internal
289    /// node per item.
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use pretty_lang::Doc;
295    ///
296    /// let doc = Doc::concat(["a", "b", "c"].map(Doc::text));
297    /// assert_eq!(doc.render(80), "abc");
298    ///
299    /// assert_eq!(Doc::concat(core::iter::empty()).render(80), "");
300    /// ```
301    #[must_use]
302    pub fn concat(docs: impl IntoIterator<Item = Doc>) -> Doc {
303        let mut iter = docs.into_iter();
304        let mut acc = match iter.next() {
305            Some(first) => first,
306            None => return Doc::nil(),
307        };
308        for doc in iter {
309            acc = acc.append(doc);
310        }
311        acc
312    }
313
314    /// Concatenate every document produced by `docs`, placing a clone of `sep`
315    /// between consecutive items (but not before the first or after the last).
316    /// Returns [`nil`](Doc::nil) for an empty iterator.
317    ///
318    /// This is the idiomatic way to render comma-separated lists, `&&`-joined
319    /// conditions, `::`-joined paths, and the like — pair it with
320    /// [`group`](Doc::group) so the whole list collapses onto one line when it
321    /// fits.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use pretty_lang::Doc;
327    ///
328    /// let path = Doc::join(Doc::text("::"), ["std", "collections", "HashMap"].map(Doc::text));
329    /// assert_eq!(path.render(80), "std::collections::HashMap");
330    ///
331    /// // With a flexible separator, the list reflows under a group.
332    /// let args = Doc::join(
333    ///     Doc::text(",").append(Doc::line()),
334    ///     ["x", "y"].map(Doc::text),
335    /// )
336    /// .group();
337    /// assert_eq!(args.render(80), "x, y");
338    /// ```
339    #[must_use]
340    pub fn join(sep: Doc, docs: impl IntoIterator<Item = Doc>) -> Doc {
341        let mut iter = docs.into_iter();
342        let mut acc = match iter.next() {
343            Some(first) => first,
344            None => return Doc::nil(),
345        };
346        for doc in iter {
347            acc = acc.append(sep.clone()).append(doc);
348        }
349        acc
350    }
351
352    /// Render this document to an owned [`String`], choosing line breaks so that
353    /// no line exceeds `width` columns where the document allows a choice.
354    ///
355    /// `width` is the target line length in Unicode scalars. Lines can still
356    /// exceed it when a single unbreakable [`text`](Doc::text) is wider than
357    /// `width`, or where the document offers no break — the renderer never
358    /// invents break points that were not described.
359    ///
360    /// # Examples
361    ///
362    /// ```
363    /// use pretty_lang::Doc;
364    ///
365    /// let doc = Doc::text("a").append(Doc::line()).append(Doc::text("b")).group();
366    /// assert_eq!(doc.render(80), "a b");
367    /// assert_eq!(doc.render(1), "a\nb");
368    /// ```
369    #[must_use]
370    pub fn render(&self, width: usize) -> String {
371        let mut out = String::new();
372        // Writing into a String is infallible, so the fmt::Result is discarded.
373        let _ = crate::render::layout(self, width, &mut out);
374        out
375    }
376
377    /// Render this document into any [`core::fmt::Write`] sink, choosing line
378    /// breaks for the target `width`. Use this to stream directly into a caller
379    /// -owned buffer and avoid the intermediate [`String`] that
380    /// [`render`](Doc::render) allocates.
381    ///
382    /// # Errors
383    ///
384    /// Returns [`core::fmt::Error`] if and only if the underlying `out` returns
385    /// an error while being written to.
386    ///
387    /// # Examples
388    ///
389    /// ```
390    /// use core::fmt::Write;
391    /// use pretty_lang::Doc;
392    ///
393    /// let doc = Doc::text("hello").append(Doc::text(" world"));
394    /// let mut buf = String::new();
395    /// doc.render_into(80, &mut buf).unwrap();
396    /// assert_eq!(buf, "hello world");
397    /// ```
398    pub fn render_into<W: core::fmt::Write>(&self, width: usize, out: &mut W) -> core::fmt::Result {
399        crate::render::layout(self, width, out)
400    }
401
402    /// Render this document into a [`std::io::Write`] sink, choosing line breaks
403    /// for the target `width`. This is the streaming counterpart to
404    /// [`render`](Doc::render) for files, sockets, and stdout.
405    ///
406    /// # Errors
407    ///
408    /// Propagates the first [`std::io::Error`] returned by `out`.
409    ///
410    /// # Examples
411    ///
412    /// ```
413    /// use pretty_lang::Doc;
414    ///
415    /// let doc = Doc::text("written to stdout");
416    /// let mut buf: Vec<u8> = Vec::new();
417    /// doc.render_writer(80, &mut buf).unwrap();
418    /// assert_eq!(buf, b"written to stdout");
419    /// ```
420    #[cfg(feature = "std")]
421    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
422    pub fn render_writer<W: std::io::Write>(
423        &self,
424        width: usize,
425        out: &mut W,
426    ) -> std::io::Result<()> {
427        crate::render::layout_io(self, width, out)
428    }
429}
430
431/// The empty document — same as [`Doc::nil`].
432impl Default for Doc {
433    #[inline]
434    fn default() -> Self {
435        Doc::nil()
436    }
437}
438
439/// Build a text document from a static string slice, without allocating.
440impl From<&'static str> for Doc {
441    #[inline]
442    fn from(s: &'static str) -> Self {
443        Doc::text(s)
444    }
445}
446
447/// Build a text document from an owned string.
448impl From<String> for Doc {
449    #[inline]
450    fn from(s: String) -> Self {
451        Doc::text(s)
452    }
453}
454
455impl Drop for Doc {
456    /// Dismantle the document iteratively when this is its last owner.
457    ///
458    /// The document is a tree of reference-counted nodes, so the derived drop
459    /// glue would recurse one call frame per level and overflow the stack on a
460    /// deeply nested document (a long chain of binary expressions, say). This
461    /// impl walks a uniquely-owned spine with an explicit heap work list
462    /// instead, keeping the actual node drops shallow. Leaves and shared nodes
463    /// take a branch-only fast path that allocates nothing.
464    fn drop(&mut self) {
465        // A leaf owns no child nodes: nothing to recurse into.
466        if matches!(
467            &*self.0,
468            Node::Nil | Node::Text(..) | Node::Line | Node::SoftLine | Node::HardLine
469        ) {
470            return;
471        }
472        // A shared internal node stays alive after this handle goes away, so
473        // dropping it will not recurse into its children.
474        if Rc::get_mut(&mut self.0).is_none() {
475            return;
476        }
477        // Uniquely-owned internal node: take its children onto a work list and
478        // dismantle the spine level by level. One `Nil` sentinel, cloned by
479        // reference count, stands in for every child slot we empty.
480        let nil = Rc::new(Node::Nil);
481        let mut stack: Vec<Rc<Node>> = Vec::new();
482        take_children(&mut self.0, &nil, &mut stack);
483        while let Some(mut node) = stack.pop() {
484            take_children(&mut node, &nil, &mut stack);
485        }
486    }
487}
488
489/// Move the child nodes of a uniquely-owned internal node onto `stack`,
490/// replacing each slot with the shared `nil` sentinel so the node itself then
491/// drops without recursing. A shared node (`get_mut` is `None`) is left alone.
492fn take_children(rc: &mut Rc<Node>, nil: &Rc<Node>, stack: &mut Vec<Rc<Node>>) {
493    let Some(node) = Rc::get_mut(rc) else { return };
494    match node {
495        Node::Cat(a, b) => {
496            stack.push(core::mem::replace(&mut a.0, nil.clone()));
497            stack.push(core::mem::replace(&mut b.0, nil.clone()));
498        }
499        Node::Nest(_, x) | Node::Group(x) => {
500            stack.push(core::mem::replace(&mut x.0, nil.clone()));
501        }
502        Node::Nil | Node::Text(..) | Node::Line | Node::SoftLine | Node::HardLine => {}
503    }
504}
505
506impl core::fmt::Debug for Doc {
507    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
508        // A structural view of the tree, useful when debugging a builder.
509        // Written iteratively so a deep document cannot overflow the stack.
510        enum Step {
511            Node(Doc),
512            Str(&'static str),
513        }
514        let mut stack = Vec::from([Step::Node(self.clone())]);
515        while let Some(step) = stack.pop() {
516            match step {
517                Step::Str(s) => f.write_str(s)?,
518                Step::Node(doc) => match &*doc.0 {
519                    Node::Nil => f.write_str("Nil")?,
520                    Node::Text(s, _) => write!(f, "Text({s:?})")?,
521                    Node::Line => f.write_str("Line")?,
522                    Node::SoftLine => f.write_str("SoftLine")?,
523                    Node::HardLine => f.write_str("HardLine")?,
524                    Node::Cat(a, b) => {
525                        f.write_str("Cat(")?;
526                        stack.push(Step::Str(")"));
527                        stack.push(Step::Node(b.clone()));
528                        stack.push(Step::Str(", "));
529                        stack.push(Step::Node(a.clone()));
530                    }
531                    Node::Nest(i, x) => {
532                        write!(f, "Nest({i}, ")?;
533                        stack.push(Step::Str(")"));
534                        stack.push(Step::Node(x.clone()));
535                    }
536                    Node::Group(x) => {
537                        f.write_str("Group(")?;
538                        stack.push(Step::Str(")"));
539                        stack.push(Step::Node(x.clone()));
540                    }
541                },
542            }
543        }
544        Ok(())
545    }
546}