Skip to main content

weaveffi_core/codegen/
writer.rs

1//! A small, deterministic code-emission toolkit shared by every generator.
2//!
3//! Before this module existed, all eleven generators built their output by hand
4//! with thousands of `out.push_str(&format!(...))` calls, threading the current
5//! indentation through every call site as literal spaces. That made the *shape*
6//! of the emitted code invisible in the Rust source and turned indentation and
7//! block nesting into a manual, bug-prone bookkeeping chore.
8//!
9//! [`CodeWriter`] owns the indentation and block scoping so a backend writes
10//! intent (`line`, `block`, `scope`) instead of whitespace. It is intentionally
11//! tiny and unopinionated: it does not reflow or pretty-print, so a backend
12//! stays in full control of the exact text it emits while losing the manual
13//! `\n`/indent bookkeeping. Output is byte-deterministic: blank lines never
14//! carry trailing whitespace, and the indent unit is fixed per writer.
15//!
16//! ```
17//! use weaveffi_core::codegen::writer::CodeWriter;
18//!
19//! let mut w = CodeWriter::new("    ");
20//! w.line("class Greeter:");
21//! w.scope(|w| {
22//!     w.line("def hello(self):");
23//!     w.scope(|w| {
24//!         w.line("return \"hi\"");
25//!     });
26//! });
27//! assert_eq!(
28//!     w.finish(),
29//!     "class Greeter:\n    def hello(self):\n        return \"hi\"\n",
30//! );
31//! ```
32
33use crate::codegen::common::{emit_doc, DocCommentStyle};
34
35/// An indentation-aware string builder for generated source code.
36///
37/// Construct one with [`CodeWriter::new`], passing the per-target indent unit
38/// (`"    "`, `"  "`, or `"\t"`). Emit lines with [`line`](Self::line), nest
39/// with [`scope`](Self::scope) / [`block`](Self::block), splice pre-rendered
40/// multi-line text with [`block_raw`](Self::block_raw), and finish with
41/// [`finish`](Self::finish).
42#[derive(Debug, Clone)]
43pub struct CodeWriter {
44    buf: String,
45    depth: usize,
46    unit: String,
47}
48
49impl CodeWriter {
50    /// Create an empty writer whose one indent level is `unit` (commonly
51    /// `"    "`, `"  "`, or `"\t"`).
52    pub fn new(unit: impl Into<String>) -> Self {
53        Self {
54            buf: String::new(),
55            depth: 0,
56            unit: unit.into(),
57        }
58    }
59
60    /// Create an empty writer with a four-space indent unit, the most common
61    /// default across the generators.
62    pub fn four_space() -> Self {
63        Self::new("    ")
64    }
65
66    /// Create an empty writer with a two-space indent unit.
67    pub fn two_space() -> Self {
68        Self::new("  ")
69    }
70
71    /// Create an empty writer with a tab indent unit.
72    pub fn tabs() -> Self {
73        Self::new("\t")
74    }
75
76    /// The current indentation depth (number of `unit`s prepended to a line).
77    pub fn depth(&self) -> usize {
78        self.depth
79    }
80
81    /// Start the writer already nested `depth` levels deep. Useful when
82    /// rendering a fragment that belongs inside an enclosing block whose
83    /// indentation the caller tracks separately (for example a backend that
84    /// still threads an explicit indent string through its render functions).
85    #[must_use]
86    pub fn with_depth(mut self, depth: usize) -> Self {
87        self.depth = depth;
88        self
89    }
90
91    /// The literal indentation prefix at the current depth. Useful when calling
92    /// a helper that takes an explicit indent string.
93    pub fn indent_str(&self) -> String {
94        self.unit.repeat(self.depth)
95    }
96
97    /// Increase the indentation depth by one level.
98    pub fn indent(&mut self) -> &mut Self {
99        self.depth += 1;
100        self
101    }
102
103    /// Decrease the indentation depth by one level. Saturates at zero so an
104    /// unbalanced `dedent` can never panic mid-render.
105    pub fn dedent(&mut self) -> &mut Self {
106        self.depth = self.depth.saturating_sub(1);
107        self
108    }
109
110    /// Write one line at the current indentation, followed by a newline.
111    ///
112    /// An empty (or whitespace-only-after-trim is *not* applied here; only a
113    /// truly empty string) argument emits a bare newline with no trailing
114    /// whitespace, so callers can use `line("")` interchangeably with
115    /// [`blank`](Self::blank).
116    pub fn line(&mut self, s: impl AsRef<str>) -> &mut Self {
117        let s = s.ref_str();
118        if s.is_empty() {
119            self.buf.push('\n');
120        } else {
121            self.buf.push_str(&self.unit.repeat(self.depth));
122            self.buf.push_str(s);
123            self.buf.push('\n');
124        }
125        self
126    }
127
128    /// Write a blank line (a single newline, never trailing whitespace).
129    pub fn blank(&mut self) -> &mut Self {
130        self.buf.push('\n');
131        self
132    }
133
134    /// Append text verbatim with no indentation and no trailing newline.
135    ///
136    /// Use this for already-fully-formatted fragments (a generated-file
137    /// prelude, a precomputed block) that must be spliced in unchanged.
138    pub fn raw(&mut self, s: impl AsRef<str>) -> &mut Self {
139        self.buf.push_str(s.ref_str());
140        self
141    }
142
143    /// Splice a multi-line fragment, re-indenting every non-empty line to the
144    /// current depth while preserving the fragment's own *relative*
145    /// indentation. A trailing newline on the fragment is honored; blank lines
146    /// stay blank (no trailing whitespace).
147    ///
148    /// This is the migration workhorse: a backend can keep a large literal
149    /// snippet as a raw string and let the writer place it at the right depth.
150    pub fn block_raw(&mut self, s: impl AsRef<str>) -> &mut Self {
151        let s = s.ref_str();
152        if s.is_empty() {
153            return self;
154        }
155        let prefix = self.unit.repeat(self.depth);
156        // Split on '\n'; a trailing newline yields a final empty segment we
157        // must not emit as its own indented line.
158        let ends_with_newline = s.ends_with('\n');
159        let mut lines = s.split('\n').peekable();
160        while let Some(line) = lines.next() {
161            let is_last = lines.peek().is_none();
162            if is_last && line.is_empty() && ends_with_newline {
163                // The empty tail produced by a trailing '\n': stop, the
164                // previous iteration already wrote that newline.
165                break;
166            }
167            if line.is_empty() {
168                self.buf.push('\n');
169            } else {
170                self.buf.push_str(&prefix);
171                self.buf.push_str(line);
172                self.buf.push('\n');
173            }
174        }
175        self
176    }
177
178    /// Run `f` with the indentation increased by one level, then restore it.
179    pub fn scope(&mut self, f: impl FnOnce(&mut Self)) -> &mut Self {
180        self.indent();
181        f(self);
182        self.dedent();
183        self
184    }
185
186    /// Emit `open`, run `f` at one deeper indent, then emit `close` at the
187    /// original indent. The canonical way to write a braced or `:`-introduced
188    /// block.
189    ///
190    /// ```
191    /// use weaveffi_core::codegen::writer::CodeWriter;
192    /// let mut w = CodeWriter::four_space();
193    /// w.block("fn main() {", "}", |w| {
194    ///     w.line("println!(\"hi\");");
195    /// });
196    /// assert_eq!(w.finish(), "fn main() {\n    println!(\"hi\");\n}\n");
197    /// ```
198    pub fn block(
199        &mut self,
200        open: impl AsRef<str>,
201        close: impl AsRef<str>,
202        f: impl FnOnce(&mut Self),
203    ) -> &mut Self {
204        self.line(open);
205        self.scope(f);
206        self.line(close);
207        self
208    }
209
210    /// Emit a doc comment for `doc` in `style` at the current indentation.
211    /// No-op when `doc` is `None` or trims to empty. Mirrors [`emit_doc`],
212    /// but indents from the writer's current depth instead of an explicit
213    /// prefix argument.
214    pub fn doc(&mut self, doc: &Option<String>, style: DocCommentStyle) -> &mut Self {
215        let prefix = self.unit.repeat(self.depth);
216        emit_doc(&mut self.buf, doc, &prefix, style);
217        self
218    }
219
220    /// Borrow the accumulated text without consuming the writer.
221    pub fn as_str(&self) -> &str {
222        &self.buf
223    }
224
225    /// True when nothing has been written yet.
226    pub fn is_empty(&self) -> bool {
227        self.buf.is_empty()
228    }
229
230    /// Consume the writer and return the accumulated source text.
231    pub fn finish(self) -> String {
232        self.buf
233    }
234}
235
236/// Tiny internal helper so `line`/`raw`/`block_raw` accept both `&str` and
237/// `String` (and `&String`) without each method taking a turbofished generic
238/// that the docs would have to explain. Not part of the public API.
239trait RefStr {
240    fn ref_str(&self) -> &str;
241}
242
243impl<T: AsRef<str>> RefStr for T {
244    fn ref_str(&self) -> &str {
245        self.as_ref()
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn line_indents_and_newline_terminates() {
255        let mut w = CodeWriter::four_space();
256        w.line("a");
257        w.indent();
258        w.line("b");
259        w.dedent();
260        w.line("c");
261        assert_eq!(w.finish(), "a\n    b\nc\n");
262    }
263
264    #[test]
265    fn empty_line_is_bare_newline() {
266        let mut w = CodeWriter::four_space();
267        w.indent();
268        w.line("");
269        w.blank();
270        w.line("x");
271        assert_eq!(w.finish(), "\n\n    x\n");
272    }
273
274    #[test]
275    fn scope_restores_depth() {
276        let mut w = CodeWriter::two_space();
277        w.line("outer");
278        w.scope(|w| {
279            w.line("inner");
280            w.scope(|w| {
281                w.line("deepest");
282            });
283            w.line("inner again");
284        });
285        w.line("outer again");
286        assert_eq!(
287            w.finish(),
288            "outer\n  inner\n    deepest\n  inner again\nouter again\n"
289        );
290    }
291
292    #[test]
293    fn block_brackets_body() {
294        let mut w = CodeWriter::four_space();
295        w.block("if (x) {", "}", |w| {
296            w.line("do_a();");
297            w.line("do_b();");
298        });
299        assert_eq!(w.finish(), "if (x) {\n    do_a();\n    do_b();\n}\n");
300    }
301
302    #[test]
303    fn nested_blocks() {
304        let mut w = CodeWriter::four_space();
305        w.block("class A:", "", |w| {
306            w.block("def f(self):", "", |w| {
307                w.line("pass");
308            });
309        });
310        // Note: an empty close just emits a bare newline.
311        assert_eq!(w.finish(), "class A:\n    def f(self):\n        pass\n\n\n");
312    }
313
314    #[test]
315    fn raw_appends_verbatim() {
316        let mut w = CodeWriter::four_space();
317        w.indent();
318        w.raw("no-indent");
319        w.raw(" continues");
320        assert_eq!(w.finish(), "no-indent continues");
321    }
322
323    #[test]
324    fn block_raw_reindents_relative_structure() {
325        let mut w = CodeWriter::four_space();
326        w.indent();
327        w.block_raw("def foo():\n    return 1\n");
328        assert_eq!(w.finish(), "    def foo():\n        return 1\n");
329    }
330
331    #[test]
332    fn block_raw_preserves_blank_lines_without_trailing_ws() {
333        let mut w = CodeWriter::two_space();
334        w.indent();
335        w.block_raw("a\n\nb\n");
336        assert_eq!(w.finish(), "  a\n\n  b\n");
337    }
338
339    #[test]
340    fn block_raw_without_trailing_newline() {
341        let mut w = CodeWriter::four_space();
342        w.block_raw("one\ntwo");
343        assert_eq!(w.finish(), "one\ntwo\n");
344    }
345
346    #[test]
347    fn block_raw_empty_is_noop() {
348        let mut w = CodeWriter::four_space();
349        w.block_raw("");
350        assert!(w.is_empty());
351    }
352
353    #[test]
354    fn doc_uses_current_indent() {
355        let mut w = CodeWriter::four_space();
356        w.indent();
357        w.doc(&Some("Hello.".to_string()), DocCommentStyle::TripleSlash);
358        w.line("fn f() {}");
359        assert_eq!(w.finish(), "    /// Hello.\n    fn f() {}\n");
360    }
361
362    #[test]
363    fn doc_none_is_noop() {
364        let mut w = CodeWriter::four_space();
365        w.doc(&None, DocCommentStyle::Hash);
366        assert!(w.is_empty());
367    }
368
369    #[test]
370    fn accepts_string_and_str() {
371        let mut w = CodeWriter::four_space();
372        let owned = String::from("owned");
373        w.line(&owned);
374        w.line("borrowed");
375        w.line(format!("fmt {}", 1));
376        assert_eq!(w.finish(), "owned\nborrowed\nfmt 1\n");
377    }
378
379    #[test]
380    fn indent_str_reflects_depth() {
381        let mut w = CodeWriter::new("  ");
382        assert_eq!(w.indent_str(), "");
383        w.indent().indent();
384        assert_eq!(w.indent_str(), "    ");
385        assert_eq!(w.depth(), 2);
386    }
387
388    #[test]
389    fn with_depth_seeds_initial_indentation() {
390        let mut w = CodeWriter::four_space().with_depth(2);
391        w.line("if x:");
392        w.scope(|w| {
393            w.line("pass");
394        });
395        assert_eq!(w.finish(), "        if x:\n            pass\n");
396    }
397
398    #[test]
399    fn dedent_saturates_at_zero() {
400        let mut w = CodeWriter::four_space();
401        w.dedent().dedent();
402        w.line("x");
403        assert_eq!(w.finish(), "x\n");
404    }
405}