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}