Skip to main content

weaveffi_core/codegen/
writer.rs

1//! A small indentation-aware string builder shared by every generator.
2//!
3//! Before this existed, each generator tracked indentation by hand: literal
4//! `"    "` / `"\t"` prefixes threaded through dozens of `push_str` calls,
5//! `format!("{indent}...")` interpolation, and ad-hoc capacity estimators.
6//! [`CodeWriter`] centralises that bookkeeping so a generator says *what*
7//! to emit and *at what nesting level*, never *how many spaces* that is.
8//!
9//! The writer is deliberately language-agnostic: it knows about lines,
10//! blank lines, and an indent stack, and nothing about braces, comments, or
11//! syntax. Generators layer their own helpers on top.
12
13/// An indentation-aware buffer of generated source text.
14///
15/// ```
16/// use weaveffi_core::codegen::writer::CodeWriter;
17/// let mut w = CodeWriter::new("    ");
18/// w.line("fn main() {");
19/// w.scope(|w| {
20///     w.line("println!(\"hi\");");
21/// });
22/// w.line("}");
23/// assert_eq!(w.finish(), "fn main() {\n    println!(\"hi\");\n}\n");
24/// ```
25#[derive(Debug, Clone)]
26pub struct CodeWriter {
27    buf: String,
28    indent_unit: String,
29    level: usize,
30}
31
32impl CodeWriter {
33    /// Create a writer that indents nested scopes with `indent_unit`
34    /// (e.g. `"    "` for four spaces, `"\t"` for a tab, `"  "` for two).
35    pub fn new(indent_unit: impl Into<String>) -> Self {
36        Self {
37            buf: String::new(),
38            indent_unit: indent_unit.into(),
39            level: 0,
40        }
41    }
42
43    /// Create a writer pre-allocated to `capacity` bytes.
44    pub fn with_capacity(indent_unit: impl Into<String>, capacity: usize) -> Self {
45        Self {
46            buf: String::with_capacity(capacity),
47            indent_unit: indent_unit.into(),
48            level: 0,
49        }
50    }
51
52    /// Seed the writer with already-rendered text (e.g. a file prelude) at
53    /// indent level zero. The text is appended verbatim.
54    pub fn push_raw(&mut self, text: impl AsRef<str>) {
55        self.buf.push_str(text.as_ref());
56    }
57
58    /// The current indentation depth, in scope levels (not characters).
59    pub fn level(&self) -> usize {
60        self.level
61    }
62
63    /// Increase the indentation level by one for subsequent lines.
64    pub fn indent(&mut self) {
65        self.level += 1;
66    }
67
68    /// Decrease the indentation level by one. Saturates at zero so an
69    /// unbalanced `dedent` cannot panic mid-generation.
70    pub fn dedent(&mut self) {
71        self.level = self.level.saturating_sub(1);
72    }
73
74    /// Run `f` with the indentation level increased by one, restoring it
75    /// afterwards even if `f` adjusts it internally.
76    pub fn scope(&mut self, f: impl FnOnce(&mut Self)) {
77        let saved = self.level;
78        self.level = saved + 1;
79        f(self);
80        self.level = saved;
81    }
82
83    /// Emit one logical line: the current indent, then `line`, then `\n`.
84    ///
85    /// If `line` itself contains `\n`, every segment is re-indented so the
86    /// caller can pass a multi-line literal and still get consistent
87    /// indentation. Empty segments stay empty (no trailing indent on blank
88    /// lines), matching what the hand-rolled emitters produced.
89    pub fn line(&mut self, line: impl AsRef<str>) {
90        let line = line.as_ref();
91        if line.is_empty() {
92            self.buf.push('\n');
93            return;
94        }
95        for segment in line.split('\n') {
96            if !segment.is_empty() {
97                self.write_indent();
98                self.buf.push_str(segment);
99            }
100            self.buf.push('\n');
101        }
102    }
103
104    /// Emit `line` only when the level is zero — convenience for the common
105    /// "top-level declaration" case that reads better than `line`.
106    pub fn top(&mut self, line: impl AsRef<str>) {
107        debug_assert_eq!(self.level, 0, "top() called inside an indented scope");
108        self.line(line);
109    }
110
111    /// Emit a single empty line with no indentation.
112    pub fn blank(&mut self) {
113        self.buf.push('\n');
114    }
115
116    /// Append verbatim text with no indentation handling. Escape hatch for
117    /// pre-formatted blocks (e.g. embedded templates) where the caller has
118    /// already taken responsibility for layout.
119    pub fn raw(&mut self, text: impl AsRef<str>) {
120        self.buf.push_str(text.as_ref());
121    }
122
123    /// Borrow the buffer accumulated so far without consuming the writer.
124    pub fn as_str(&self) -> &str {
125        &self.buf
126    }
127
128    /// True when nothing has been written yet.
129    pub fn is_empty(&self) -> bool {
130        self.buf.is_empty()
131    }
132
133    /// Consume the writer and return the accumulated source text.
134    pub fn finish(self) -> String {
135        self.buf
136    }
137
138    fn write_indent(&mut self) {
139        for _ in 0..self.level {
140            self.buf.push_str(&self.indent_unit);
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn empty_writer_is_empty() {
151        let w = CodeWriter::new("    ");
152        assert!(w.is_empty());
153        assert_eq!(w.finish(), "");
154    }
155
156    #[test]
157    fn line_appends_newline() {
158        let mut w = CodeWriter::new("    ");
159        w.line("hello");
160        assert_eq!(w.finish(), "hello\n");
161    }
162
163    #[test]
164    fn scope_indents_with_unit() {
165        let mut w = CodeWriter::new("    ");
166        w.line("a");
167        w.scope(|w| w.line("b"));
168        w.line("c");
169        assert_eq!(w.finish(), "a\n    b\nc\n");
170    }
171
172    #[test]
173    fn nested_scopes_stack() {
174        let mut w = CodeWriter::new("  ");
175        w.line("a");
176        w.scope(|w| {
177            w.line("b");
178            w.scope(|w| w.line("c"));
179        });
180        assert_eq!(w.finish(), "a\n  b\n    c\n");
181    }
182
183    #[test]
184    fn tab_indent_unit() {
185        let mut w = CodeWriter::new("\t");
186        w.scope(|w| w.line("x"));
187        assert_eq!(w.finish(), "\tx\n");
188    }
189
190    #[test]
191    fn blank_has_no_indent() {
192        let mut w = CodeWriter::new("    ");
193        w.scope(|w| {
194            w.line("x");
195            w.blank();
196            w.line("y");
197        });
198        assert_eq!(w.finish(), "    x\n\n    y\n");
199    }
200
201    #[test]
202    fn multiline_line_reindents_each_segment() {
203        let mut w = CodeWriter::new("  ");
204        w.scope(|w| w.line("a\nb\nc"));
205        assert_eq!(w.finish(), "  a\n  b\n  c\n");
206    }
207
208    #[test]
209    fn multiline_keeps_interior_blanks_unindented() {
210        let mut w = CodeWriter::new("  ");
211        w.scope(|w| w.line("a\n\nb"));
212        assert_eq!(w.finish(), "  a\n\n  b\n");
213    }
214
215    #[test]
216    fn manual_indent_dedent_saturates() {
217        let mut w = CodeWriter::new("  ");
218        w.dedent(); // would underflow; must saturate at 0
219        w.line("a");
220        w.indent();
221        w.line("b");
222        assert_eq!(w.finish(), "a\n  b\n");
223    }
224
225    #[test]
226    fn push_raw_and_raw_bypass_indentation() {
227        let mut w = CodeWriter::new("    ");
228        w.push_raw("// prelude\n");
229        w.scope(|w| w.raw("verbatim"));
230        assert_eq!(w.finish(), "// prelude\nverbatim");
231    }
232}