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}