Skip to main content

drawlang_syntax/
fmt.rs

1//! Canonical formatter. One statement per line, two-space indent, comments
2//! preserved, at most one consecutive blank line. Idempotent:
3//! `fmt(fmt(x)) == fmt(x)`.
4//!
5//! Leaf values (strings, numbers, expressions, paths) are reprinted from
6//! their source spans, so escapes and interpolations round-trip exactly.
7
8use crate::ast::*;
9use crate::span::Span;
10
11const INLINE_MAX: usize = 80;
12
13pub fn format(file: &File, src: &str) -> String {
14    let f = Formatter { src };
15    let mut out = String::new();
16    let version = file
17        .header
18        .as_ref()
19        .map(|h| h.version.as_str())
20        .unwrap_or("0.1");
21    out.push_str(&format!("drawl {version}\n"));
22
23    let mut first = true;
24    for stmt in &file.stmts {
25        // Top-level statements get a blank line between them by default;
26        // consecutive property-like lines stay together if the author had
27        // no blank line and no comments.
28        let want_blank = if first {
29            true
30        } else {
31            stmt.trivia.blank_before || !stmt.trivia.leading.is_empty() || is_block_stmt(stmt)
32        };
33        if want_blank {
34            out.push('\n');
35        }
36        first = false;
37        f.write_stmt(&mut out, stmt, 0);
38    }
39    // Exactly one trailing newline.
40    while out.ends_with("\n\n") {
41        out.pop();
42    }
43    if !out.ends_with('\n') {
44        out.push('\n');
45    }
46    out
47}
48
49fn is_block_stmt(stmt: &Stmt) -> bool {
50    matches!(
51        stmt.kind,
52        StmtKind::Canvas(_)
53            | StmtKind::Def(_)
54            | StmtKind::Group(_)
55            | StmtKind::Class(_)
56            | StmtKind::Constrain(_)
57            | StmtKind::For(_)
58    )
59}
60
61struct Formatter<'a> {
62    src: &'a str,
63}
64
65impl<'a> Formatter<'a> {
66    fn snip(&self, span: Span) -> &str {
67        let s = span.start.min(self.src.len());
68        let e = span.end.min(self.src.len());
69        self.src[s..e].trim()
70    }
71
72    fn write_stmt(&self, out: &mut String, stmt: &Stmt, depth: usize) {
73        let indent = "  ".repeat(depth);
74        for c in &stmt.trivia.leading {
75            out.push_str(&indent);
76            if c.is_empty() {
77                out.push_str("//\n");
78            } else {
79                out.push_str(&format!("// {c}\n"));
80            }
81        }
82        // Orphan-comment sentinel: comments only, no statement.
83        if let StmtKind::Prop(p) = &stmt.kind {
84            if p.key.is_empty() {
85                return;
86            }
87        }
88
89        let trailing = stmt
90            .trivia
91            .trailing
92            .as_ref()
93            .map(|c| format!(" // {c}"))
94            .unwrap_or_default();
95
96        match &stmt.kind {
97            StmtKind::Canvas(block) => {
98                out.push_str(&format!("{indent}canvas {{\n"));
99                self.write_block_stmts(out, block, depth + 1);
100                out.push_str(&format!("{indent}}}{trailing}\n"));
101            }
102            StmtKind::Def(d) => {
103                let params: Vec<&str> = d.params.iter().map(|p| p.name.as_str()).collect();
104                out.push_str(&format!(
105                    "{indent}def {}({}) {{\n",
106                    d.name.name,
107                    params.join(", ")
108                ));
109                self.write_block_stmts(out, &d.body, depth + 1);
110                out.push_str(&format!("{indent}}}{trailing}\n"));
111            }
112            StmtKind::Group(g) => {
113                let label = g
114                    .label
115                    .as_ref()
116                    .map(|l| format!(" {}", self.snip(l.span)))
117                    .unwrap_or_default();
118                out.push_str(&format!("{indent}group {}{label} {{\n", g.name.name));
119                self.write_block_stmts(out, &g.body, depth + 1);
120                out.push_str(&format!("{indent}}}{trailing}\n"));
121            }
122            StmtKind::Class(c) => {
123                if let Some(line) = self.inline_block(&c.body) {
124                    out.push_str(&format!("{indent}class {} {line}{trailing}\n", c.name.name));
125                } else {
126                    out.push_str(&format!("{indent}class {} {{\n", c.name.name));
127                    self.write_block_stmts(out, &c.body, depth + 1);
128                    out.push_str(&format!("{indent}}}{trailing}\n"));
129                }
130            }
131            StmtKind::Constrain(cs) => {
132                out.push_str(&format!("{indent}constrain {{\n"));
133                let inner = "  ".repeat(depth + 1);
134                for c in cs {
135                    for lc in &c.trivia.leading {
136                        out.push_str(&format!("{inner}// {lc}\n"));
137                    }
138                    let t = c
139                        .trivia
140                        .trailing
141                        .as_ref()
142                        .map(|x| format!(" // {x}"))
143                        .unwrap_or_default();
144                    out.push_str(&format!("{inner}{}{t}\n", self.snip(c.span)));
145                }
146                out.push_str(&format!("{indent}}}{trailing}\n"));
147            }
148            StmtKind::Pin(p) => {
149                out.push_str(&format!(
150                    "{indent}pin {} at ({}, {}){trailing}\n",
151                    self.snip(p.target.span),
152                    self.snip(p.x.span),
153                    self.snip(p.y.span)
154                ));
155            }
156            StmtKind::For(f) => {
157                if let Some(line) = self.inline_for(f) {
158                    if indent.len() + line.len() <= INLINE_MAX {
159                        out.push_str(&format!("{indent}{line}{trailing}\n"));
160                        return;
161                    }
162                }
163                out.push_str(&format!(
164                    "{indent}for {} in {}..{} {{\n",
165                    f.var.name,
166                    self.snip(f.start.span),
167                    self.snip(f.end.span)
168                ));
169                self.write_block_stmts(out, &f.body, depth + 1);
170                out.push_str(&format!("{indent}}}{trailing}\n"));
171            }
172            StmtKind::Port(p) => {
173                if let Some(line) = self.inline_block(&p.body) {
174                    out.push_str(&format!("{indent}port {} {line}{trailing}\n", p.name.name));
175                } else {
176                    out.push_str(&format!("{indent}port {} {{\n", p.name.name));
177                    self.write_block_stmts(out, &p.body, depth + 1);
178                    out.push_str(&format!("{indent}}}{trailing}\n"));
179                }
180            }
181            StmtKind::Prop(p) => {
182                out.push_str(&format!("{indent}{}{trailing}\n", self.prop_line(p)));
183            }
184            StmtKind::Node(n) => {
185                if let Some(line) = self.inline_node(n) {
186                    if indent.len() + line.len() <= INLINE_MAX {
187                        out.push_str(&format!("{indent}{line}{trailing}\n"));
188                        return;
189                    }
190                }
191                self.write_node_multiline(out, n, depth, &trailing);
192            }
193            StmtKind::Edge(e) => {
194                let op = match e.op {
195                    EdgeOp::Forward => "->",
196                    EdgeOp::Bidirectional => "<->",
197                };
198                let label = e
199                    .label
200                    .as_ref()
201                    .map(|l| format!(" : {}", self.snip(l.span)))
202                    .unwrap_or_default();
203                let props = match &e.props {
204                    Some(b) => match self.inline_block(b) {
205                        Some(line) => format!(" {line}"),
206                        None => format!(" {}", self.snip(b.span)),
207                    },
208                    None => String::new(),
209                };
210                out.push_str(&format!(
211                    "{indent}{} {op} {}{label}{props}{trailing}\n",
212                    self.snip(e.from.span),
213                    self.snip(e.to.span)
214                ));
215            }
216        }
217    }
218
219    fn write_block_stmts(&self, out: &mut String, block: &Block, depth: usize) {
220        let mut first = true;
221        for stmt in &block.stmts {
222            if !first && (stmt.trivia.blank_before || !stmt.trivia.leading.is_empty()) {
223                out.push('\n');
224            }
225            first = false;
226            self.write_stmt(out, stmt, depth);
227        }
228    }
229
230    fn write_node_multiline(&self, out: &mut String, n: &Node, depth: usize, trailing: &str) {
231        let indent = "  ".repeat(depth);
232        match &n.kind {
233            NodeKind::Plain { body } => {
234                let name = n.name.as_ref().expect("plain nodes are named");
235                if body.stmts.is_empty() {
236                    out.push_str(&format!("{indent}{}{trailing}\n", name.name));
237                    return;
238                }
239                out.push_str(&format!("{indent}{} {{\n", name.name));
240                self.write_block_stmts(out, body, depth + 1);
241                out.push_str(&format!("{indent}}}{trailing}\n"));
242            }
243            NodeKind::Container { ctype, body, .. } => {
244                let name = n.name.as_ref().expect("containers are named");
245                let kw = match ctype {
246                    ContainerType::Row => "row".to_string(),
247                    ContainerType::Column => "column".to_string(),
248                    ContainerType::Grid { cols, rows } => format!("grid {cols}x{rows}"),
249                };
250                out.push_str(&format!("{indent}{}: {kw} {{\n", name.name));
251                self.write_block_stmts(out, body, depth + 1);
252                out.push_str(&format!("{indent}}}{trailing}\n"));
253            }
254            NodeKind::Call { callee, args, body } => {
255                let args: Vec<&str> = args.iter().map(|a| self.snip(a.span)).collect();
256                let head = match &n.name {
257                    Some(name) => format!("{}: {}({})", name.name, callee.name, args.join(", ")),
258                    None => format!("{}({})", callee.name, args.join(", ")),
259                };
260                match body {
261                    Some(b) if !b.stmts.is_empty() => {
262                        out.push_str(&format!("{indent}{head} {{\n"));
263                        self.write_block_stmts(out, b, depth + 1);
264                        out.push_str(&format!("{indent}}}{trailing}\n"));
265                    }
266                    _ => out.push_str(&format!("{indent}{head}{trailing}\n")),
267                }
268            }
269        }
270    }
271
272    fn prop_line(&self, p: &Prop) -> String {
273        let key: Vec<&str> = p.key.iter().map(|k| k.name.as_str()).collect();
274        format!("{}: {}", key.join("."), self.snip(p.value.span()))
275    }
276
277    /// `{ a: 1; b: 2 }` if the block is only 1–2 comment-free properties.
278    fn inline_block(&self, block: &Block) -> Option<String> {
279        if block.stmts.is_empty() {
280            return None;
281        }
282        if block.stmts.len() > 2 {
283            return None;
284        }
285        let mut parts = Vec::new();
286        for s in &block.stmts {
287            if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
288                return None;
289            }
290            match &s.kind {
291                StmtKind::Prop(p) if !p.key.is_empty() => parts.push(self.prop_line(p)),
292                _ => return None,
293            }
294        }
295        let line = format!("{{ {} }}", parts.join("; "));
296        (line.len() <= INLINE_MAX - 10).then_some(line)
297    }
298
299    /// `name { label: "x" }`, `name`, `gpu(i)`, or `g0: gpu(0)`.
300    fn inline_node(&self, n: &Node) -> Option<String> {
301        match &n.kind {
302            NodeKind::Plain { body } => {
303                let name = n.name.as_ref()?;
304                if body.stmts.is_empty() {
305                    return Some(name.name.clone());
306                }
307                // Inline only when every stmt is a prop or a port that fits.
308                if body.stmts.len() > 2 {
309                    return None;
310                }
311                let mut parts = Vec::new();
312                for s in &body.stmts {
313                    if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
314                        return None;
315                    }
316                    match &s.kind {
317                        StmtKind::Prop(p) if !p.key.is_empty() => parts.push(self.prop_line(p)),
318                        StmtKind::Port(p) => {
319                            let inner = self.inline_block(&p.body)?;
320                            parts.push(format!("port {} {inner}", p.name.name));
321                        }
322                        _ => return None,
323                    }
324                }
325                Some(format!("{} {{ {} }}", name.name, parts.join("; ")))
326            }
327            NodeKind::Container { ctype, body, .. } => {
328                let name = n.name.as_ref()?;
329                if body.stmts.len() != 1 {
330                    return None;
331                }
332                let inner_stmt = &body.stmts[0];
333                if !inner_stmt.trivia.leading.is_empty() || inner_stmt.trivia.trailing.is_some() {
334                    return None;
335                }
336                let inner = match &inner_stmt.kind {
337                    StmtKind::For(f) => self.inline_for(f)?,
338                    StmtKind::Node(inner_n) => self.inline_node(inner_n)?,
339                    _ => return None,
340                };
341                let kw = match ctype {
342                    ContainerType::Row => "row".to_string(),
343                    ContainerType::Column => "column".to_string(),
344                    ContainerType::Grid { cols, rows } => format!("grid {cols}x{rows}"),
345                };
346                Some(format!("{}: {kw} {{ {inner} }}", name.name))
347            }
348            NodeKind::Call { callee, args, body } => {
349                let args: Vec<&str> = args.iter().map(|a| self.snip(a.span)).collect();
350                let head = match &n.name {
351                    Some(name) => format!("{}: {}({})", name.name, callee.name, args.join(", ")),
352                    None => format!("{}({})", callee.name, args.join(", ")),
353                };
354                match body {
355                    None => Some(head),
356                    Some(b) if b.stmts.is_empty() => Some(head),
357                    Some(b) => {
358                        let inner = self.inline_block(b)?;
359                        Some(format!("{head} {inner}"))
360                    }
361                }
362            }
363        }
364    }
365
366    fn inline_for(&self, f: &For) -> Option<String> {
367        if f.body.stmts.len() != 1 {
368            return None;
369        }
370        let s = &f.body.stmts[0];
371        if !s.trivia.leading.is_empty() || s.trivia.trailing.is_some() {
372            return None;
373        }
374        let inner = match &s.kind {
375            StmtKind::Node(n) => self.inline_node(n)?,
376            StmtKind::Edge(e) => {
377                let op = match e.op {
378                    EdgeOp::Forward => "->",
379                    EdgeOp::Bidirectional => "<->",
380                };
381                let label = e
382                    .label
383                    .as_ref()
384                    .map(|l| format!(" : {}", self.snip(l.span)))
385                    .unwrap_or_default();
386                let props = match &e.props {
387                    Some(b) => format!(" {}", self.inline_block(b)?),
388                    None => String::new(),
389                };
390                format!(
391                    "{} {op} {}{label}{props}",
392                    self.snip(e.from.span),
393                    self.snip(e.to.span)
394                )
395            }
396            _ => return None,
397        };
398        Some(format!(
399            "for {} in {}..{} {{ {inner} }}",
400            f.var.name,
401            self.snip(f.start.span),
402            self.snip(f.end.span)
403        ))
404    }
405}