pretty_simple/
lib.rs

1// Copyright 2025 Cameron Swords
2// SPDX-License-Identifier: Apache-2.0
3
4use std::rc::Rc;
5
6use once_cell::unsync::Lazy;
7
8mod tests;
9
10// -------------------------------------------------------------------------------------------------
11// Main Trait
12// -------------------------------------------------------------------------------------------------
13
14pub trait ToDoc {
15    /// Convert the type into a `Doc`.
16    fn to_doc(&self) -> Doc;
17    fn render(&self, width: i16) -> String {
18        self.to_doc().render(width)
19    }
20}
21
22// -------------------------------------------------------------------------------------------------
23// Helpers
24// -------------------------------------------------------------------------------------------------
25
26/// Convert an iterator of items to a `Doc` by rendering each item with `ToDoc` and
27/// interspersing `separator` between them.
28///
29/// Returns [`Doc::nil()`] if the iterator is empty.
30///
31/// # Example
32/// ```rust
33///use pretty_simple::*;
34///
35/// #[derive(Debug)]
36/// struct Item(&'static str);
37///
38/// impl ToDoc for Item {
39///     fn to_doc(&self) -> Doc { Doc::text(self.0) }
40/// }
41///
42/// let xs = [Item("a"), Item("b"), Item("c")];
43///
44/// let doc = to_list(xs.iter(), Doc::text(","));
45///
46/// assert_eq!(doc.render(80), "a,b,c");
47/// ```
48pub fn to_list<'a, T>(docs: impl IntoIterator<Item = &'a T>, separator: Doc) -> Doc
49where
50    T: ToDoc + 'a,
51{
52    let mut iter = docs.into_iter();
53    if let Some(first) = iter.next() {
54        let mut output = first.to_doc();
55        for next in iter {
56            output = output.concat(separator.clone()).concat(next.to_doc());
57        }
58        output
59    } else {
60        Doc::nil()
61    }
62}
63
64// -------------------------------------------------------------------------------------------------
65// Documents
66// -------------------------------------------------------------------------------------------------
67
68pub struct Doc(Rc<DocInner>);
69
70type DocFn = Rc<dyn Fn(i16) -> Doc + 'static>;
71
72enum DocInner {
73    Empty,
74    Text(String),
75    Line, // soft line break
76    Concat(Doc, Doc),
77    Nest(i16, Doc),
78    Alt(Doc, Doc),
79    Nesting(DocFn),
80    Column(DocFn),
81}
82
83// This is a bit of an absue of notation, but it will make our lives a touch simpler.
84impl DocInner {
85    fn into_doc(self) -> Doc {
86        Doc(Rc::new(self))
87    }
88}
89
90impl Clone for Doc {
91    fn clone(&self) -> Self {
92        Doc(Rc::clone(&self.0))
93    }
94}
95// -----------------------------------------------
96// Thread Locals
97// -----------------------------------------------
98
99thread_local! {
100    static NIL_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Empty));
101    static SPACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(" ".to_string())));
102    static COMMA_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(",".to_string())));
103    static LINE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Line));
104    static SOFTLINE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Alt(Doc::space(), Doc::line())));
105    static SOFTLINE_EMPTY_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Alt(Doc::nil(), Doc::line())));
106    static LPAREN_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("(".to_string())));
107    static RPAREN_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(")".to_string())));
108    static LANGLE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("<".to_string())));
109    static RANGLE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text(">".to_string())));
110    static LBRACKET_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("[".to_string())));
111    static RBRACKET_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("]".to_string())));
112    static LBRACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("{".to_string())));
113    static RBRACE_INNER: Lazy<Rc<DocInner>> = Lazy::new(|| Rc::new(DocInner::Text("}".to_string())));
114}
115
116impl Doc {
117    // -------------------------------------------
118    // Core Constructors
119    // -------------------------------------------
120
121    /// The empty document.
122    ///
123    /// Renders to nothing and acts as the identity element for [`Doc::concat`].
124    pub fn nil() -> Doc {
125        NIL_INNER.with(|lazy| Doc(Rc::clone(lazy)))
126    }
127
128    /// A single ASCII space as a document (`" "`).
129    pub fn space() -> Doc {
130        SPACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
131    }
132
133    /// A single ASCII comma as a document (`","`).
134    pub fn comma() -> Doc {
135        COMMA_INNER.with(|lazy| Doc(Rc::clone(lazy)))
136    }
137
138    /// A hard line break.
139    ///
140    /// When rendered, this always breaks the line and sets the cursor to the current
141    /// indentation level tracked by nesting/indentation combinators.
142    pub fn line() -> Doc {
143        LINE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
144    }
145
146    /// A soft line break that becomes a space if the layout fits the given width,
147    /// or a newline otherwise.
148    ///
149    /// This is equivalent to `Alt(space, line)` in Wadler/Leijen pretty‑printing.
150    pub fn softline() -> Doc {
151        SOFTLINE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
152    }
153
154    /// A soft line break that becomes empty if the layout fits, or a newline
155    /// otherwise.
156    ///
157    /// Useful for optional separators (e.g., trailing commas off).
158    pub fn softline_empty() -> Doc {
159        SOFTLINE_EMPTY_INNER.with(|lazy| Doc(Rc::clone(lazy)))
160    }
161
162    /// Construct a document from raw text.
163    ///
164    /// The string is inserted verbatim; it will not contain line breaks unless
165    /// they are present in the string itself (which generally should be avoided
166    /// in pretty‑printing docs).
167    pub fn text<S: Into<String>>(str: S) -> Doc {
168        DocInner::Text(str.into()).into_doc()
169    }
170
171    /// Concatenate two documents without inserting any separator.
172    pub fn concat(self, other: Doc) -> Doc {
173        DocInner::Concat(self, other).into_doc()
174    }
175
176    /// Increase the nesting (indentation) level for all lines that follow a newline
177    /// within the given document by `depth` columns.
178    pub fn nest(self, depth: i16) -> Doc {
179        DocInner::Nest(depth, self).into_doc()
180    }
181
182    // `<+>` from Haskell
183    //
184    // Concatenates the two documents with a space between them.
185    pub fn concat_space(self, other: Doc) -> Doc {
186        self.concat(Doc::space()).concat(other)
187    }
188
189    /// Creates an `alt` set, preferring the first one if it fits and devolving to the second if it
190    /// does not.
191    pub fn alt(self, other: Doc) -> Doc {
192        DocInner::Alt(self, other).into_doc()
193    }
194
195    /// Try to render `self` on a single line by first flattening all soft breaks;
196    /// if that does not fit within the current width, fall back to the original
197    /// (multi‑line) layout.
198    ///
199    /// This is the standard `group` combinator from pretty‑printing literature.
200    pub fn group(self) -> Doc {
201        match &*self.0 {
202            DocInner::Alt(_, _) => self,
203            _ => DocInner::Alt(self.clone().flatten(), self).into_doc(),
204        }
205    }
206
207    fn flatten(self) -> Doc {
208        match &*self.0 {
209            DocInner::Empty | DocInner::Text(_) => self,
210            DocInner::Line => Doc::space(),
211            DocInner::Concat(x, y) => {
212                DocInner::Concat(x.clone().flatten(), y.clone().flatten()).into_doc()
213            }
214            DocInner::Nest(_, inner) => inner.clone().flatten(),
215            DocInner::Alt(flat, _) => flat.clone().flatten(),
216            DocInner::Column(f) => {
217                let f = Rc::clone(f);
218                let f = Rc::new(move |i| f(i).flatten());
219                Doc(Rc::new(DocInner::Column(f)))
220            }
221            DocInner::Nesting(f) => {
222                let f = Rc::clone(f);
223                let f = Rc::new(move |i| f(i).flatten());
224                Doc(Rc::new(DocInner::Nesting(f)))
225            }
226        }
227    }
228
229    /// Create a document whose contents are computed from the **current output column**.
230    ///
231    /// The closure receives the current cursor column (0‑based) and returns the
232    /// document to splice in at that point. The closure is stored as a `'static`
233    /// callable via `Rc`, so capture owned data in it.
234    ///
235    /// See also [`Doc::nesting`].
236    pub fn column<F>(f: F) -> Doc
237    where
238        F: Fn(i16) -> Doc + 'static,
239    {
240        let f: DocFn = Rc::new(f);
241        DocInner::Column(f).into_doc()
242    }
243
244    /// Create a document whose contents are computed from the **current nesting level**.
245    ///
246    /// The closure receives the current indentation level (the `i` tracked by the
247    /// renderer) and returns the document to splice in. Use this to align content
248    /// relative to the current indent.
249    ///
250    /// See also [`Doc::column`].
251    pub fn nesting<F>(f: F) -> Doc
252    where
253        F: Fn(i16) -> Doc + 'static,
254    {
255        let f: DocFn = Rc::new(f);
256        DocInner::Nesting(f).into_doc()
257    }
258
259    // -------------------------------------------
260    // Helpers
261    // -------------------------------------------
262
263    /// Fold an iterator of documents by repeatedly combining adjacent items with
264    /// `concat_f`.
265    ///
266    /// This is a generalized form of [`hcat`](Self::hcat), [`hsep`](Self::hsep),
267    /// and [`vsep`](Self::vsep). Returns [`Doc::nil()`] for an empty iterator.
268    pub fn concat_with<F>(docs: impl IntoIterator<Item = Doc>, concat_f: F) -> Doc
269    where
270        F: Fn(Doc, Doc) -> Doc,
271    {
272        let mut iter = docs.into_iter();
273        if let Some(first) = iter.next() {
274            let mut output = first;
275            for next in iter {
276                output = concat_f(output, next);
277            }
278            output
279        } else {
280            Doc::nil()
281        }
282    }
283
284    /// A convenience for “hanging” indentation: `self.nest(i).align()`.
285    ///
286    /// Subsequent lines align under the first character after an `i`‑space indent.
287    pub fn hang(self, i: i16) -> Doc {
288        self.nest(i).align()
289    }
290
291    /// Indent `self` by `i` spaces, and use a hanging layout so subsequent lines
292    /// align under the first non‑space character.
293    ///
294    /// Equivalent to `Doc::spaces(i).concat(self).hang(i)`.
295    pub fn indent(self, i: i16) -> Doc {
296        Doc::spaces(i).concat(self).hang(i)
297    }
298
299    /// Align subsequent lines to the current column.
300    ///
301    /// Useful for layouts like:
302    /// ```text
303    /// key: value that
304    ///      wraps across lines
305    /// ```
306    /// Internally implemented via [`Doc::column`] and [`Doc::nesting`].
307    pub fn align(self) -> Doc {
308        // Move an owned clone into the closures so they’re 'static.
309        Doc::column({
310            let base = self.clone();
311            move |k| {
312                let base2 = base.clone();
313                Doc::nesting(move |i| base2.clone().nest(k - i))
314            }
315        })
316    }
317
318    /// Produce `i` spaces as a document (`" ".repeat(i)`), with fast paths for 0 and 1.
319    pub fn spaces(i: i16) -> Doc {
320        match i {
321            0 => Doc::nil(),
322            1 => Doc::space(),
323            n => Doc::text(" ".repeat(n as usize)),
324        }
325    }
326
327    /// Horizontally separate an iterator of documents with single spaces.
328    ///
329    /// Equivalent to interspersing [`Doc::space()`] and concatenating.
330    pub fn hsep(docs: impl IntoIterator<Item = Doc>) -> Doc {
331        Doc::concat_with(docs, |x, y| x.concat_space(y))
332    }
333
334    /// Vertically separate an iterator of documents with hard newlines.
335    ///
336    /// Equivalent to interspersing [`Doc::line()`] and concatenating.
337    pub fn vsep(docs: impl IntoIterator<Item = Doc>) -> Doc {
338        Doc::concat_with(docs, |x, y| x.concat(Doc::line()).concat(y))
339    }
340
341    // Tries laying the elements out with spaces, or vertically if they do not fit.
342    pub fn sep(docs: impl IntoIterator<Item = Doc>) -> Doc {
343        Doc::vsep(docs).group()
344    }
345
346    /// Concatenate an iterator of documents without separators (left‑associative).
347    pub fn hcat(docs: impl IntoIterator<Item = Doc>) -> Doc {
348        Doc::concat_with(docs, |x, y| x.concat(y))
349    }
350
351    /// Concatenate `docs`, inserting `separator` between each adjacent pair.
352    ///
353    /// Returns [`Doc::nil()`] if `docs` is empty.
354    pub fn intersperse(docs: impl IntoIterator<Item = Doc>, separator: Doc) -> Doc {
355        let mut iter = docs.into_iter();
356        if let Some(first) = iter.next() {
357            let mut output = first;
358            for next in iter {
359                output = output.concat(separator.clone()).concat(next);
360            }
361            output
362        } else {
363            Doc::nil()
364        }
365    }
366
367    /// Surround `self` with `(` and `)` (parentheses).
368    pub fn parens(self) -> Doc {
369        Self::lparen().concat(self).concat(Self::rparen())
370    }
371
372    /// Surround `self` with `<` and `>` (angle brackets).
373    pub fn angles(self) -> Doc {
374        Self::langle().concat(self).concat(Self::rangle())
375    }
376
377    /// Surround `self` with `[` and `]` (square brackets).
378    pub fn brackets(self) -> Doc {
379        Self::lbracket().concat(self).concat(Self::rbracket())
380    }
381
382    /// Surround `self` with `{` and `}` (curly braces).
383    pub fn braces(self) -> Doc {
384        Self::lbrace().concat(self).concat(Self::rbrace())
385    }
386
387    /// Render `self` as a typical block:
388    ///
389    /// ```text
390    /// {start}
391    ///     {self (indented, grouped)}
392    /// {end}
393    /// ```
394    ///
395    /// Uses a 4‑space indent and inserts newlines before and after the block body.
396    pub fn block(self, start: Doc, end: Doc) -> Doc {
397        start
398            .concat(Doc::line())
399            .concat(self.indent(4).group())
400            .concat(Doc::line())
401            .concat(end)
402    }
403
404    /// Fill a la Wadler
405    /// This
406    pub fn fill(xs: &[Doc]) -> Doc {
407        Self::fill_core(xs, 0, false)
408    }
409
410    /// `head_flat` means: treat xs[i] as already flattened (because caller passed `flatten y : zs`)
411    fn fill_core(xs: &[Doc], i: usize, head_flat: bool) -> Doc {
412        if i >= xs.len() {
413            return Doc::nil();
414        }
415        let n = xs.len() - i;
416        if n == 1 {
417            let mut d = xs[i].clone();
418            if head_flat {
419                d = d.flatten();
420            }
421            return d;
422        }
423
424        // We have at least two: x = xs[i], y = xs[i+1]
425        let x = xs[i].clone();
426        let y_is_head = i + 1; // head of the recursive tail
427
428        // Left branch: (flatten x <+> fill (flatten y : zs))
429        // If the current head is already flattened, don't double-flatten.
430        let x_flat = if head_flat {
431            x.clone()
432        } else {
433            x.clone().flatten()
434        };
435        let left = x_flat
436            .concat(Doc::space())
437            // Next level's head (y) must be treated as already flattened
438            .concat(Self::fill_core(xs, y_is_head, true));
439
440        // Right branch: (x </> fill (y : zs))
441        // If head_flat is true, x is already flattened; use it as-is.
442        let x_for_right = if head_flat { x } else { xs[i].clone() };
443        let right = x_for_right
444            .concat(Doc::line())
445            .concat(Self::fill_core(xs, y_is_head, false));
446
447        left.alt(right)
448    }
449
450    // -------------------------------------------
451    // Constant Constructors
452    // -------------------------------------------
453
454    /// The `<` document.
455    pub fn lparen() -> Doc {
456        LPAREN_INNER.with(|lazy| Doc(Rc::clone(lazy)))
457    }
458
459    /// The `>` document.
460    pub fn rparen() -> Doc {
461        RPAREN_INNER.with(|lazy| Doc(Rc::clone(lazy)))
462    }
463
464    /// The `<` document.
465    pub fn langle() -> Doc {
466        LANGLE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
467    }
468
469    /// The `>` document.
470    pub fn rangle() -> Doc {
471        RANGLE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
472    }
473
474    /// The `[` document.
475    pub fn lbracket() -> Doc {
476        LBRACKET_INNER.with(|lazy| Doc(Rc::clone(lazy)))
477    }
478
479    /// The `]` document.
480    pub fn rbracket() -> Doc {
481        RBRACKET_INNER.with(|lazy| Doc(Rc::clone(lazy)))
482    }
483
484    /// The `{` document.
485    pub fn lbrace() -> Doc {
486        LBRACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
487    }
488
489    /// The `}` document.
490    pub fn rbrace() -> Doc {
491        RBRACE_INNER.with(|lazy| Doc(Rc::clone(lazy)))
492    }
493
494    // -------------------------------------------
495    // Rendering
496    // -------------------------------------------
497
498    /// Render the document to a `String` using the given maximum line `width`.
499    ///
500    /// Soft breaks choose between space/newline based on whether the flattened
501    /// alternative fits within the remaining width; hard breaks always break.
502    /// The algorithm is a variant of Wadler/Leijen pretty‑printing.
503    pub fn render(self, width: i16) -> String {
504        let rendered = self.best(width);
505        let output = rendered.render();
506        // std::mem::forget(rendered);
507        output.unwrap()
508    }
509
510    fn best(self, width: i16) -> Render {
511        use DocInner as DI;
512
513        enum Cons {
514            Cell { head: (i16, Doc), tail: Rc<Cons> },
515            Nil,
516        }
517
518        fn cons(head: (i16, Doc), tail: Rc<Cons>) -> Rc<Cons> {
519            Rc::new(Cons::Cell { head, tail })
520        }
521
522        // A non-allocating, non-recursive "does it fit?" that peeks ahead.
523        // Returns false if we'd exceed `remaining` or hit a hard Line.
524        fn fits(mut remaining: i16, mut cursor: i16, mut docs: Rc<Cons>) -> bool {
525            while let Cons::Cell {
526                head: (i, doc),
527                tail,
528            } = &*docs
529            {
530                match &*doc.0 {
531                    DI::Line => return true,
532                    DI::Empty => {
533                        docs = tail.clone();
534                    }
535                    DI::Text(s) => {
536                        let s_len = s.len() as i16;
537                        if s_len > remaining {
538                            return false;
539                        };
540                        remaining -= s_len;
541                        let Some(new_cursor) = cursor.checked_add(s_len) else {
542                            return false;
543                        };
544                        cursor = new_cursor;
545                        docs = tail.clone();
546                    }
547                    DI::Concat(x, y) => {
548                        docs = cons((*i, x.clone()), cons((*i, y.clone()), tail.clone()));
549                    }
550                    DI::Nest(j, inner) => {
551                        docs = cons((i + j, inner.clone()), tail.clone());
552                    }
553                    DI::Alt(flat, _doc2) => {
554                        docs = cons((*i, flat.clone()), tail.clone());
555                    }
556                    DI::Column(f) => {
557                        docs = cons((*i, f(cursor)), tail.clone());
558                    }
559                    DI::Nesting(f) => {
560                        docs = cons((*i, f(*i)), tail.clone());
561                    }
562                }
563            }
564            true
565        }
566
567        let mut docs = cons((0, self), Rc::new(Cons::Nil));
568        let mut cursor = 0i16;
569        let mut out: Vec<RenderPart> = vec![];
570
571        while let Cons::Cell { head, tail } = &*docs {
572            let (indent, doc) = head;
573            match &*doc.0 {
574                DI::Empty => {
575                    docs = tail.clone();
576                }
577                DI::Text(s) => {
578                    out.push(RenderPart::Text(s.to_string()));
579                    cursor = cursor.saturating_add(s.len() as i16);
580                    docs = tail.clone();
581                }
582                DI::Concat(x, y) => {
583                    docs = cons(
584                        (*indent, x.clone()),
585                        cons((*indent, y.clone()), tail.clone()),
586                    );
587                }
588                DI::Nest(j, inner) => {
589                    docs = cons((indent + j, inner.clone()), tail.clone());
590                }
591                DI::Line => {
592                    out.push(RenderPart::Line(*indent));
593                    cursor = *indent;
594                    docs = tail.clone();
595                }
596                DI::Alt(flat, alt) => {
597                    let flat = cons((*indent, flat.clone()), tail.clone());
598                    if fits(width, cursor, flat.clone()) {
599                        docs = flat;
600                    } else {
601                        docs = cons((*indent, alt.clone()), tail.clone());
602                    }
603                }
604                DI::Column(f) => {
605                    docs = cons((*indent, f(cursor)), tail.clone());
606                }
607                DI::Nesting(f) => {
608                    docs = cons((*indent, f(*indent)), tail.clone());
609                }
610            }
611        }
612
613        Render(out)
614    }
615}
616
617// -------------------------------------------------------------------------------------------------
618// Rendering
619// -------------------------------------------------------------------------------------------------
620
621enum RenderPart {
622    Line(i16),
623    Text(String),
624}
625
626struct Render(Vec<RenderPart>);
627
628impl Render {
629    fn render(&self) -> Result<String, std::fmt::Error> {
630        use std::fmt::Write;
631        let renders = &self.0;
632        let mut output = String::new();
633        for render in renders.iter() {
634            match render {
635                RenderPart::Line(i) => {
636                    writeln!(&mut output)?;
637                    for _n in 0..*i {
638                        write!(&mut output, " ")?;
639                    }
640                }
641                RenderPart::Text(s) => {
642                    write!(&mut output, "{}", s)?;
643                }
644            }
645        }
646        Ok(output)
647    }
648}