beck_syntax/
print.rs

1//! The printer — one `Node` tree, two surfaces.
2//!
3//! [`docs/02-syntax.md`](../../../../../docs/02-syntax.md) §2.2: "`beck fmt --sexpr orders.beck`
4//! emits the canonical Lisp form. `beck fmt --py orders.sx` emits the Python form." §2.8 asks for
5//! it early because "every later phase uses it": macro expansion dumps, `beck ast`, and the error
6//! renderer all print `Node`s.
7//!
8//! Round-tripping is lossless *modulo formatting*: `parse(print(parse(src)))` is structurally equal
9//! to `parse(src)`, which is the property `tests/roundtrip.rs` asserts over the corpus.
10
11use std::fmt::Write as _;
12
13use crate::node::{sym, Head, Lit, Node};
14
15/// Canonical S-expressions, one line. The notation the sketch is written in.
16pub fn to_sexpr(n: &Node) -> String {
17    let mut out = String::new();
18    write_sexpr(&mut out, n);
19    out
20}
21
22/// Canonical S-expressions, broken across lines where a form is long.
23pub fn to_sexpr_pretty(n: &Node) -> String {
24    let mut out = String::new();
25    write_sexpr_pretty(&mut out, n, 0);
26    out.push('\n');
27    out
28}
29
30fn write_atom(out: &mut String, n: &Node) {
31    match &n.head {
32        Head::Sym(s) => {
33            let _ = write!(out, "{}", s.name);
34            // Hygiene scopes are printed only when present, so ordinary source round-trips
35            // unchanged and expansion dumps stay legible.
36            if !s.scopes.is_empty() {
37                let _ = write!(out, "{:?}", s.scopes);
38            }
39        }
40        Head::Lit(l) => write_lit(out, l),
41    }
42}
43
44fn write_lit(out: &mut String, l: &Lit) {
45    match l {
46        Lit::Int(v) => {
47            let _ = write!(out, "{v}");
48        }
49        Lit::Float(v) => {
50            if v.fract() == 0.0 && v.is_finite() {
51                let _ = write!(out, "{v:.1}");
52            } else {
53                let _ = write!(out, "{v}");
54            }
55        }
56        Lit::Bool(v) => {
57            let _ = write!(out, "{v}");
58        }
59        Lit::Keyword(k) => {
60            let _ = write!(out, ":{k}");
61        }
62        Lit::Str(s) => write_string(out, s),
63    }
64}
65
66fn write_string(out: &mut String, s: &str) {
67    out.push('"');
68    for c in s.chars() {
69        match c {
70            '"' => out.push_str("\\\""),
71            '\\' => out.push_str("\\\\"),
72            '\n' => out.push_str("\\n"),
73            '\t' => out.push_str("\\t"),
74            '\r' => out.push_str("\\r"),
75            _ => out.push(c),
76        }
77    }
78    out.push('"');
79}
80
81fn write_sexpr(out: &mut String, n: &Node) {
82    if !n.applied {
83        write_atom(out, n);
84        return;
85    }
86    out.push('(');
87    write_atom(out, n);
88    for a in &n.args {
89        out.push(' ');
90        write_sexpr(out, a);
91    }
92    out.push(')');
93}
94
95fn write_sexpr_pretty(out: &mut String, n: &Node, indent: usize) {
96    let flat = to_sexpr(n);
97    // A documented argument forces the broken form: a comment needs a line of its own, so a form
98    // short enough to print flat still has to be broken to keep its documentation.
99    let documented = n.args.iter().any(|a| a.meta.doc.is_some());
100    if !n.applied || (flat.len() + indent <= 96 && !documented) {
101        out.push_str(&flat);
102        return;
103    }
104    out.push('(');
105    write_atom(out, n);
106    let inner = indent + 2;
107    let pad = " ".repeat(inner);
108    for a in &n.args {
109        if let Some(doc) = &a.meta.doc {
110            for line in crate::doc::render(doc, crate::doc::SEXPR_MARKER, &pad).lines() {
111                out.push('\n');
112                out.push_str(line);
113            }
114        }
115        out.push('\n');
116        out.push_str(&pad);
117        write_sexpr_pretty(out, a, inner);
118    }
119    out.push(')');
120}
121
122/// The Python surface. This is what `beck fmt` writes, and §2.6's style rules ("`snake_case`
123/// values, `PascalCase` types … enforced by `beck fmt`") are applied by the *formatter*, not here:
124/// this function prints faithfully, so that printing an already-renamed tree is idempotent.
125pub fn to_python(n: &Node) -> String {
126    let mut p = Py {
127        out: String::new(),
128        indent: 0,
129        trailing: None,
130    };
131    if n.is_form(sym::MODULE) {
132        for (i, item) in n.args.iter().skip(1).enumerate() {
133            if i > 0 {
134                p.out.push('\n');
135            }
136            p.item(item);
137            p.comments_after(item);
138        }
139    } else {
140        p.item(n);
141        p.comments_after(n);
142    }
143    // Whatever the file ended with, and anything `crate::doc` could not place. A module node's own
144    // comments are the file's tail, so they come last and at column zero.
145    if n.meta
146        .comments
147        .as_deref()
148        .is_some_and(|c| !c.after.is_empty())
149    {
150        p.out.push('\n');
151        p.comments_after(n);
152    }
153    p.out
154}
155
156struct Py {
157    out: String,
158    indent: usize,
159    /// A comment to hang off the end of the next line printed.
160    ///
161    /// A statement may print several lines — an `if` and its block — and the comment belongs to
162    /// the one the node started on, so this is taken by the first `line` after it is set.
163    trailing: Option<String>,
164}
165
166impl Py {
167    fn line(&mut self, s: &str) {
168        for _ in 0..self.indent {
169            self.out.push_str("    ");
170        }
171        self.out.push_str(s);
172        if let Some(c) = self.trailing.take() {
173            self.out.push_str("  ");
174            self.out.push_str(&c);
175        }
176        self.out.push('\n');
177    }
178
179    /// The comments written above this node, and the one written at the end of its line.
180    ///
181    /// Called at the places a node is printed on a line of its own — `item`, `stmt`, a match arm, a
182    /// model field, a union variant — because those are the positions a comment can hold. A
183    /// comment inside an expression has nowhere to go that would re-parse, and `crate::doc` does
184    /// not attach one there.
185    ///
186    /// **Ordinary comments print above the doc comment**, whichever order they were written in.
187    /// That is a normalisation and the one place this printer moves a comment: documentation
188    /// belongs immediately above the thing it documents, and a note about the note goes above
189    /// both. It is stable — formatting the result again changes nothing.
190    fn comments_before(&mut self, n: &Node) {
191        let Some(c) = n.meta.comments.as_deref() else {
192            return;
193        };
194        for line in &c.before {
195            // A blank line inside a comment block is a blank line, not four spaces of one.
196            if line.is_empty() {
197                self.out.push('\n');
198            } else {
199                self.line(line);
200            }
201        }
202        self.trailing = c.trailing.as_deref().map(str::to_string);
203    }
204
205    /// The comments below this node that had nothing after them in their block.
206    fn comments_after(&mut self, n: &Node) {
207        let Some(c) = n.meta.comments.as_deref() else {
208            return;
209        };
210        for line in &c.after {
211            if line.is_empty() {
212                self.out.push('\n');
213            } else {
214                self.line(line);
215            }
216        }
217    }
218
219    /// Emit the node's doc comment, if it has one, at the current indentation.
220    ///
221    /// Ordinary comments are dropped by the lexer and so cannot survive `beck fmt`; a doc comment
222    /// is [`crate::Meta`], so it can, and formatting a documented module has to give it back.
223    fn docs(&mut self, n: &Node) {
224        let Some(doc) = n.meta.doc.clone() else {
225            return;
226        };
227        let indent = "    ".repeat(self.indent);
228        self.out
229            .push_str(&crate::doc::render(&doc, crate::doc::PY_MARKER, &indent));
230    }
231
232    fn item(&mut self, n: &Node) {
233        self.comments_before(n);
234        self.docs(n);
235        self.item_body(n);
236    }
237
238    /// A declaration, with its comments already printed.
239    ///
240    /// Split from [`Py::item`] for the reason [`Py::stmt`] is split from `stmt_body`: a top-level
241    /// signal declaration is an item *and* a statement, and `item` hands it to the statement
242    /// printer — so a single entry point would print its comments once on the way in and again on
243    /// the way through, which is what the tree-wide idempotence gate caught.
244    fn item_body(&mut self, n: &Node) {
245        match n.head_name() {
246            Some(sym::DECORATE) => {
247                let deco = self.expr(&n.args[0]);
248                self.line(&format!("@{deco}"));
249                self.item(&n.args[1]);
250            }
251            Some(sym::DEF) => self.def(n),
252            Some(head @ (sym::MACRO | sym::TYPED_MACRO)) => {
253                let name = self.expr(&n.args[0]);
254                let params = self.params(&n.args[1]);
255                let typed = if head == sym::TYPED_MACRO {
256                    "typed "
257                } else {
258                    ""
259                };
260                self.line(&format!("{typed}macro {name}({params}):"));
261                self.body(&n.args[2]);
262            }
263            Some(sym::MODEL) => {
264                let name = self.expr(&n.args[0]);
265                let typarams = self.typarams(n);
266                self.line(&format!("model {name}{typarams}:"));
267                self.indent += 1;
268                if n.args.len() == 2 {
269                    self.line("pass");
270                }
271                for f in &n.args[2..] {
272                    // A field is a line of its own, so it holds comments like any other.
273                    self.comments_before(f);
274                    self.docs(f);
275                    let fname = self.expr(&f.args[0]);
276                    let fty = self.type_expr(&f.args[1]);
277                    self.line(&format!("{fname}: {fty}"));
278                }
279                self.indent -= 1;
280            }
281            Some(sym::UNION) => {
282                let name = self.expr(&n.args[0]);
283                let typarams = self.typarams(n);
284                self.line(&format!("union {name}{typarams}:"));
285                self.indent += 1;
286                for v in &n.args[2..] {
287                    // A variant is a line of its own, so it holds comments like any other.
288                    self.comments_before(v);
289                    self.docs(v);
290                    let vname = self.expr(&v.args[0]);
291                    if v.args.len() == 1 {
292                        self.line(&vname);
293                    } else {
294                        let fields: Vec<String> = v.args[1..]
295                            .iter()
296                            .map(|f| {
297                                format!("{}: {}", self.expr(&f.args[0]), self.type_expr(&f.args[1]))
298                            })
299                            .collect();
300                        self.line(&format!("{vname}({})", fields.join(", ")));
301                    }
302                }
303                self.indent -= 1;
304            }
305            Some(sym::TRAIT) => {
306                let name = self.expr(&n.args[0]);
307                self.line(&format!("trait {name}:"));
308                self.indent += 1;
309                for m in &n.args[1..] {
310                    self.item(m);
311                }
312                self.indent -= 1;
313            }
314            Some(sym::IMPL) => {
315                let name = self.expr(&n.args[0]);
316                let typarams = self.typarams(n);
317                let ty = self.type_expr(&n.args[2]);
318                self.line(&format!("impl{typarams} {name} for {ty}:"));
319                self.indent += 1;
320                for m in &n.args[3..] {
321                    self.item(m);
322                }
323                self.indent -= 1;
324            }
325            Some(sym::TYPE) => {
326                let name = self.expr(&n.args[0]);
327                let typarams = self.typarams(n);
328                let ty = self.type_expr(&n.args[2]);
329                self.line(&format!("type {name}{typarams} = {ty}"));
330            }
331            Some(sym::NEWTYPE) => {
332                let name = self.expr(&n.args[0]);
333                let typarams = self.typarams(n);
334                let ty = self.type_expr(&n.args[2]);
335                self.line(&format!("type {name}{typarams} = newtype[{ty}]"));
336            }
337            Some(sym::IMPORT) => {
338                let path = self.expr(&n.args[0]);
339                self.line(&format!("import {path}"));
340            }
341            Some(sym::ROW) if n.args.len() >= 2 => {
342                let name = self.expr(&n.args[0]);
343                let atoms: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
344                self.line(&format!("row {name} = {}", atoms.join(", ")));
345            }
346            Some(sym::IDENTITY) if n.args.len() == 1 => {
347                let provider = self.expr(&n.args[0]);
348                self.line(&format!("identity = {provider}"));
349            }
350            Some(sym::TEST) => {
351                let name = self.expr(&n.args[0]);
352                self.line(&format!("test {name}:"));
353                self.body(&n.args[1]);
354            }
355            Some(sym::PROPERTY) if n.args.len() == 3 => {
356                let name = self.expr(&n.args[0]);
357                let params = self.params(&n.args[1]);
358                self.line(&format!("property {name}({params}):"));
359                self.body(&n.args[2]);
360            }
361            _ => self.stmt_body(n),
362        }
363    }
364
365    /// `[T, U]` or `[T: Show + Eq]` from the list at `args[1]`, or the empty string when there is
366    /// nothing to quantify.
367    fn typarams(&mut self, n: &Node) -> String {
368        let Some(t) = n.args.get(1).filter(|t| !t.args.is_empty()) else {
369            return String::new();
370        };
371        let names: Vec<String> = t
372            .args
373            .clone()
374            .iter()
375            .map(|a| {
376                if !a.is_form(sym::ANNOT) || a.args.len() < 2 {
377                    return self.expr(a);
378                }
379                let bounds: Vec<String> = a.args[1..].iter().map(|b| self.expr(b)).collect();
380                format!("{}: {}", self.expr(&a.args[0]), bounds.join(" + "))
381            })
382            .collect();
383        format!("[{}]", names.join(", "))
384    }
385
386    fn def(&mut self, n: &Node) {
387        let name = self.expr(&n.args[0]);
388        let typarams = self.typarams(n);
389        let params = self.params(&n.args[2]);
390        let ret = n
391            .args
392            .get(3)
393            .filter(|r| !r.args.is_empty())
394            .map(|r| format!(" -> {}", self.type_expr(&r.args[0])))
395            .unwrap_or_default();
396        let uses = n
397            .args
398            .get(4)
399            .filter(|u| !u.args.is_empty())
400            .map(|u| {
401                let items: Vec<String> = u.args.iter().map(|e| self.expr(e)).collect();
402                format!(" uses {}", items.join(", "))
403            })
404            .unwrap_or_default();
405        match n.args.get(5) {
406            Some(body) => {
407                self.line(&format!("def {name}{typarams}({params}){ret}{uses}:"));
408                self.body(body);
409            }
410            // A declaration: a trait's method signature, or a line of a `.becki` interface (§3.6).
411            // It prints without a colon, which is what it parses back from.
412            None => self.line(&format!("def {name}{typarams}({params}){ret}{uses}")),
413        }
414    }
415
416    fn params(&self, n: &Node) -> String {
417        n.args
418            .iter()
419            .map(|p| {
420                if p.is_form(sym::ANNOT) {
421                    format!("{}: {}", self.expr(&p.args[0]), self.type_expr(&p.args[1]))
422                } else {
423                    self.expr(p)
424                }
425            })
426            .collect::<Vec<_>>()
427            .join(", ")
428    }
429
430    /// The pattern of a `case`, and its guard when it has one.
431    fn case_head(&self, arm: &Node) -> String {
432        let pat = self.expr(&arm.args[0]);
433        match arm.args.get(2) {
434            Some(g) => format!("{pat} if {}", self.expr(g)),
435            None => pat,
436        }
437    }
438
439    fn body(&mut self, n: &Node) {
440        self.indent += 1;
441        if n.args.is_empty() {
442            self.line("pass");
443        }
444        for s in &n.args {
445            self.stmt(s);
446        }
447        self.indent -= 1;
448    }
449
450    fn stmt(&mut self, n: &Node) {
451        self.comments_before(n);
452        self.stmt_body(n);
453        self.comments_after(n);
454    }
455
456    fn stmt_body(&mut self, n: &Node) {
457        match n.head_name() {
458            Some(sym::DO) => {
459                for s in &n.args {
460                    self.stmt(s);
461                }
462            }
463            Some(sym::RETURN) => match n.args.first() {
464                // `return ui:` + block. The block rule applies in final position (§2.7 only
465                // forbids a block-form call as a *non-final argument*), so the printer has to
466                // reproduce it — printing `return ui(do=quote(...))` would not re-parse, because
467                // `quote` is a block form and `;`-joined statements are not surface syntax.
468                // `return quote:` + template — the shape every macro body has (§2.4).
469                // `return try:` + block, for the same reason as the `quote` case below it: the
470                // handler carries an indented body, and `try((do …))` is not surface syntax.
471                Some(e) if e.is_form(sym::TRY) && e.args.len() == 1 => {
472                    self.line("return try:");
473                    self.body(&e.args[0]);
474                }
475                Some(e) if e.is_form(sym::PARALLEL) && e.args.len() == 1 => {
476                    self.line("return parallel:");
477                    self.body(&e.args[0]);
478                }
479                Some(e) if e.is_form(sym::QUOTE) && e.args.len() == 1 => {
480                    self.line("return quote:");
481                    let body = &e.args[0];
482                    if body.is_form(sym::DO) {
483                        self.body(body);
484                    } else {
485                        self.indent += 1;
486                        self.stmt(body);
487                        self.indent -= 1;
488                    }
489                }
490                Some(e) => match split_block_call(e) {
491                    Some((head, args, block)) => {
492                        let rendered = self.call_text(head, &args);
493                        self.line(&format!("return {rendered}:"));
494                        self.body(&block);
495                    }
496                    None => {
497                        let e = self.expr(e);
498                        self.line(&format!("return {e}"));
499                    }
500                },
501                None => self.line("return"),
502            },
503            Some(sym::LET) | Some(sym::VAR)
504                if n.args.len() == 2 && split_block_call(&n.args[1]).is_some() =>
505            {
506                let (head, args, block) =
507                    split_block_call(&n.args[1]).expect("checked by the guard");
508                let target = self.expr(&n.args[0]);
509                let keyword = if n.is_form(sym::VAR) { "var " } else { "" };
510                let rendered = self.call_text(head, &args);
511                self.line(&format!("{keyword}{target} = {rendered}:"));
512                self.body(&block);
513            }
514            // `x = try:`, `x = parallel:` and `x = quote:` — a block form bound to a name. None is
515            // a call, so `split_block_call` above cannot see it, and printing `x = try((do …))`
516            // would not re-parse. All three are expressions (`docs/27` §27.7, §2.4), so all three
517            // can appear here — and a macro that builds syntax by folding into a binding, which is
518            // how `lib/json.beck`'s `derive_json` builds a record field by field, writes the third.
519            Some(sym::LET) | Some(sym::VAR)
520                if n.args.len() == 2
521                    && (n.args[1].is_form(sym::TRY)
522                        || n.args[1].is_form(sym::PARALLEL)
523                        || n.args[1].is_form(sym::QUOTE))
524                    && n.args[1].args.len() == 1 =>
525            {
526                let keyword = if n.is_form(sym::VAR) { "var " } else { "" };
527                let target = self.expr(&n.args[0]);
528                let head = n.args[1].head_name().unwrap_or(sym::TRY);
529                self.line(&format!("{keyword}{target} = {head}:"));
530                let body = &n.args[1].args[0];
531                // A `quote:` of one expression is not a `do`, and `body` would print its argument
532                // list rather than a block.
533                if body.is_form(sym::DO) {
534                    self.body(body);
535                } else {
536                    self.indent += 1;
537                    self.stmt(body);
538                    self.indent -= 1;
539                }
540            }
541            Some(sym::LET) if n.args.len() == 2 => {
542                let t = &n.args[0];
543                let target = if t.is_form(sym::ANNOT) {
544                    format!("{}: {}", self.expr(&t.args[0]), self.type_expr(&t.args[1]))
545                } else {
546                    self.expr(t)
547                };
548                let v = self.expr(&n.args[1]);
549                self.line(&format!("{target} = {v}"));
550            }
551            Some(sym::VAR) if n.args.len() == 2 => {
552                let t = &n.args[0];
553                let target = if t.is_form(sym::ANNOT) {
554                    format!("{}: {}", self.expr(&t.args[0]), self.type_expr(&t.args[1]))
555                } else {
556                    self.expr(t)
557                };
558                let v = self.expr(&n.args[1]);
559                self.line(&format!("var {target} = {v}"));
560            }
561            Some(sym::IF) if n.args.len() >= 2 && n.args[1].is_form(sym::DO) => {
562                let c = self.expr(&n.args[0]);
563                self.line(&format!("if {c}:"));
564                self.body(&n.args[1]);
565                if let Some(alt) = n.args.get(2) {
566                    // `elif` is an `else` whose only statement is another `if`.
567                    if alt.args.len() == 1 && alt.args[0].is_form(sym::IF) {
568                        let inner = &alt.args[0];
569                        let mut s = String::new();
570                        std::mem::swap(&mut self.out, &mut s);
571                        self.stmt(inner);
572                        std::mem::swap(&mut self.out, &mut s);
573                        let pad = "    ".repeat(self.indent);
574                        let rewritten = s.replacen(&format!("{pad}if "), &format!("{pad}elif "), 1);
575                        self.out.push_str(&rewritten);
576                    } else {
577                        self.line("else:");
578                        self.body(alt);
579                    }
580                }
581            }
582            Some(sym::FOR) if n.args.len() == 3 => {
583                let v = self.expr(&n.args[0]);
584                let seq = self.expr(&n.args[1]);
585                self.line(&format!("for {v} in {seq}:"));
586                self.body(&n.args[2]);
587            }
588            Some(sym::WHILE) if n.args.len() == 2 => {
589                let c = self.expr(&n.args[0]);
590                self.line(&format!("while {c}:"));
591                self.body(&n.args[1]);
592            }
593            Some(sym::MATCH) if !n.args.is_empty() => {
594                let s = self.expr(&n.args[0]);
595                self.line(&format!("match {s}:"));
596                self.indent += 1;
597                for arm in &n.args[1..] {
598                    // An arm is a statement position too — a comment above `case` is about that
599                    // arm — even though an arm is not a statement anywhere else in this printer.
600                    self.comments_before(arm);
601                    self.line(&format!("case {}:", self.case_head(arm)));
602                    self.body(&arm.args[1]);
603                    self.comments_after(arm);
604                }
605                self.indent -= 1;
606            }
607            // `try:` + block. Like `ui:` it carries an indented body, so it prints as one rather
608            // than as a call — `try((do …))` is not surface syntax and would not re-parse.
609            Some(sym::TRY) if n.args.len() == 1 => {
610                self.line("try:");
611                self.body(&n.args[0]);
612            }
613            // `parallel:` + block, for the same reason: the scope's children *are* the indented
614            // statements, so there is no call form to print it as.
615            Some(sym::PARALLEL) if n.args.len() == 1 => {
616                self.line("parallel:");
617                self.body(&n.args[0]);
618            }
619            Some(sym::ROW | sym::IDENTITY) => self.item(n),
620            Some(
621                sym::DEF | sym::MACRO | sym::TYPED_MACRO | sym::MODEL | sym::UNION | sym::TYPE,
622            )
623            | Some(sym::NEWTYPE)
624            | Some(sym::TRAIT | sym::IMPL | sym::IMPORT | sym::DECORATE | sym::TEST)
625            | Some(sym::PROPERTY) => self.item(n),
626
627            // ---- §21.2's clauses. Each prints back as the line it was written as, because
628            // `parse(print(parse(src))) == parse(src)` is asserted over the corpus and a test block
629            // is part of the corpus now.
630            Some(sym::GIVEN) if !n.args.is_empty() => {
631                let events = self.expr(&n.args[0]);
632                match n.args.get(1) {
633                    Some(actor) => {
634                        let a = self.expr(actor);
635                        self.line(&format!("given {events} by {a}"));
636                    }
637                    None => self.line(&format!("given {events}")),
638                }
639            }
640            Some(sym::WHEN) if n.args.len() >= 2 => {
641                let cmds: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
642                let cmds = cmds.join(", ");
643                match session_of(&n.args[0]) {
644                    Some(session) => self.line(&format!("when {session} sends {cmds}")),
645                    None => self.line(&format!("when {cmds}")),
646                }
647            }
648            Some(sym::EXPECT) if n.args.len() == 1 => {
649                // `expect Ok(…)`/`expect Err(…)` parsed as `result == …`; printing the desugared
650                // form is what makes the round-trip a fixed point rather than an oscillation.
651                let e = self.expr(&n.args[0]);
652                self.line(&format!("expect {e}"));
653            }
654            Some(sym::EXPECT_CONTAINS) if !n.args.is_empty() => {
655                let needle = self.expr(&n.args[0]);
656                match n.args.get(1).and_then(session_of) {
657                    Some(session) => {
658                        self.line(&format!("expect page({session}) contains {needle}"))
659                    }
660                    None => self.line(&format!("expect page contains {needle}")),
661                }
662            }
663            Some(sym::EXPECT_SNAPSHOT) if n.args.len() == 2 => {
664                let subject = match session_of(&n.args[1]) {
665                    Some(session) => format!("page({session})"),
666                    None => "page".to_string(),
667                };
668                match n.args[0].as_str_lit() {
669                    Some(name) => {
670                        self.line(&format!("expect {subject} matches snapshot \"{name}\""))
671                    }
672                    None => self.line(&format!("expect {subject} matches snapshot")),
673                }
674            }
675            Some(sym::EXPECT_FOLD) if !n.args.is_empty() => {
676                let events = self.expr(&n.args[0]);
677                match n.args.get(1).and_then(|a| a.as_str_lit()) {
678                    Some(actor) => {
679                        self.line(&format!("expect state == fold_of {events} by \"{actor}\""))
680                    }
681                    None => self.line(&format!("expect state == fold_of {events}")),
682                }
683            }
684            Some(sym::EXPECT_PLACE) if n.args.len() == 2 => {
685                let what = self.expr(&n.args[0]);
686                let tier = self.expr(&n.args[1]);
687                self.line(&format!("expect place({what}) == {tier}"));
688            }
689            Some(sym::EXPECT_FLOW) if n.args.len() == 2 => {
690                let ty = self.expr(&n.args[0]);
691                let tier = self.expr(&n.args[1]);
692                self.line(&format!("expect flow({ty}) reaches nothing on {tier}"));
693            }
694            Some(sym::EXPECT_WIRE) if n.args.len() == 1 => {
695                let path = self.expr(&n.args[0]);
696                self.line(&format!("expect wire_compatible_with {path}"));
697            }
698            Some(sym::EXPECT_EFFECT) if n.args.len() == 2 => {
699                let atom = n.args[0].as_str_lit().unwrap_or_default().to_string();
700                let how = &n.args[1];
701                match how.head_name() {
702                    Some("times") if how.args.len() == 1 => {
703                        match how.args[0].as_lit() {
704                            Some(Lit::Int(1)) => self.line(&format!("expect {atom} once")),
705                            Some(Lit::Int(k)) => self.line(&format!("expect {atom} times {k}")),
706                            _ => self.line(&format!("expect {atom} once")),
707                        };
708                    }
709                    Some("with") if how.args.len() == 1 => {
710                        let v = self.expr(&how.args[0]);
711                        self.line(&format!("expect {atom} with {v}"));
712                    }
713                    _ => self.line(&format!("expect no {atom}")),
714                }
715            }
716            Some(sym::STUB) if n.args.len() == 2 => {
717                let atom = n.args[0].as_str_lit().unwrap_or_default().to_string();
718                let body = &n.args[1];
719                if body.is_form(sym::STUB_ARMS) {
720                    self.line(&format!("stub {atom}:"));
721                    self.indent += 1;
722                    for arm in &body.args {
723                        self.comments_before(arm);
724                        self.line(&format!("case {}:", self.case_head(arm)));
725                        self.body(&arm.args[1]);
726                    }
727                    self.indent -= 1;
728                } else if body.is_form(sym::DO) {
729                    self.line(&format!("stub {atom}:"));
730                    self.body(body);
731                } else {
732                    let v = self.expr(body);
733                    self.line(&format!("stub {atom}: {v}"));
734                }
735            }
736            _ => {
737                // A call with a `do=` block prints back in block form; anything else is an
738                // expression statement.
739                if let Some((head, args, block)) = split_block_call(n) {
740                    let rendered = self.call_text(head, &args);
741                    self.line(&format!("{rendered}:"));
742                    self.body(&block);
743                } else {
744                    let e = self.expr(n);
745                    self.line(&e);
746                }
747            }
748        }
749    }
750
751    fn call_text(&self, head: &str, args: &[Node]) -> String {
752        if args.is_empty() {
753            return head.to_string();
754        }
755        let rendered: Vec<String> = args.iter().map(|a| self.expr(a)).collect();
756        format!("{head}({})", rendered.join(", "))
757    }
758
759    fn type_expr(&self, n: &Node) -> String {
760        match n.head_name() {
761            // `>= 1`, not `>= 2`: a `fn-type` node is its parameters followed by its result, so a
762            // function type taking **no** arguments has exactly one. `docs/63` §63.3 found `() -> T`
763            // missing from the parser and the checker; the printer kept the same off-by-one, and
764            // printed `fn-type[T]` — the internal head, in a file `beck fmt` had just written.
765            Some("fn-type") if !n.args.is_empty() => {
766                let params: Vec<String> = n.args[..n.args.len() - 1]
767                    .iter()
768                    .map(|a| self.type_expr(a))
769                    .collect();
770                format!(
771                    "({}) -> {}",
772                    params.join(", "),
773                    self.type_expr(&n.args[n.args.len() - 1])
774                )
775            }
776            _ if !n.applied => {
777                let mut s = String::new();
778                write_atom(&mut s, n);
779                s
780            }
781            _ => {
782                let mut head = String::new();
783                write_atom(&mut head, n);
784                let args: Vec<String> = n.args.iter().map(|a| self.type_expr(a)).collect();
785                format!("{head}[{}]", args.join(", "))
786            }
787        }
788    }
789
790    /// The body of a block form, as one line.
791    ///
792    /// A `do` wrapping a single statement is §2.3's single-line block, which is what every block
793    /// form written as an operand is. A `do` with several is not expressible inline — the surface
794    /// has no separator for statements — so it is printed as its statements joined by `; `, which
795    /// does not re-parse and is why `beck-cli/tests/roundtrip.rs` would fail on one. Nothing in
796    /// this tree writes that shape; a program that does is a gap in the surface rather than in the
797    /// printer, and the failing test is where that argument gets had.
798    fn block_expr(&self, n: &Node) -> String {
799        if n.is_form(sym::DO) {
800            let parts: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
801            return parts.join("; ");
802        }
803        self.expr(n)
804    }
805
806    fn expr(&self, n: &Node) -> String {
807        if !n.applied {
808            let mut s = String::new();
809            write_atom(&mut s, n);
810            return s;
811        }
812        let head = n.head_name().unwrap_or("");
813        match head {
814            "not" if n.args.len() == 1 => format!("not {}", self.expr(&n.args[0])),
815            sym::RAISE if n.args.len() == 1 => format!("raise {}", self.expr(&n.args[0])),
816            // `try:` and `parallel:` are **expressions** (`docs/27` §27.7), so they turn up as
817            // operands — `expect (try: benchmark()) == Ok(True)` is how seven files in this tree
818            // assert a fallible answer. §2.3's single-line block form is the notation for one here,
819            // and the parentheses are what let it be an operand at all. Without this they printed
820            // as `try(…)`, which is not surface syntax: `beck fmt` emitted a program that does not
821            // compile, in ten files, until `beck-cli/tests/roundtrip.rs` existed to say so.
822            sym::TRY | sym::PARALLEL if n.args.len() == 1 => {
823                format!("({head}: {})", self.block_expr(&n.args[0]))
824            }
825            "negate" if n.args.len() == 1 => format!("-{}", self.expr(&n.args[0])),
826            sym::UNQUOTE if n.args.len() == 1 => format!("${}", self.expr(&n.args[0])),
827            sym::SPLICE if n.args.len() == 1 => format!("$*{}", self.expr(&n.args[0])),
828            "and" | "or" | "|" | "@" | "==" | "!=" | "<" | "<=" | ">" | ">=" | "+" | "-" | "*"
829            | "/" | "%"
830                if n.args.len() == 2 =>
831            {
832                format!(
833                    "({} {head} {})",
834                    self.expr(&n.args[0]),
835                    self.expr(&n.args[1])
836                )
837            }
838            "contains" if n.args.len() == 2 => {
839                format!("({} in {})", self.expr(&n.args[0]), self.expr(&n.args[1]))
840            }
841            "index" if n.args.len() == 2 => {
842                format!("{}[{}]", self.expr(&n.args[0]), self.expr(&n.args[1]))
843            }
844            // Parenthesised, like every binary operator above it and for the same reason: without
845            // them `a + b if c else d` reads back as `a + (b if c else d)`, which is a different
846            // program that still parses. `clbg/fannkuchredux.beck` is where that showed up.
847            sym::IF if n.args.len() == 3 && !n.args[1].is_form(sym::DO) => format!(
848                "({} if {} else {})",
849                self.expr(&n.args[1]),
850                self.expr(&n.args[0]),
851                self.expr(&n.args[2])
852            ),
853            sym::DOT if n.args.len() == 2 => {
854                format!("{}.{}", self.expr(&n.args[0]), self.expr(&n.args[1]))
855            }
856            sym::DOT if n.args.len() > 2 => {
857                let args: Vec<String> = n.args[2..].iter().map(|a| self.expr(a)).collect();
858                format!(
859                    "{}.{}({})",
860                    self.expr(&n.args[0]),
861                    self.expr(&n.args[1]),
862                    args.join(", ")
863                )
864            }
865            sym::KW_ARG if n.args.len() == 2 => {
866                format!("{}={}", self.expr(&n.args[0]), self.expr(&n.args[1]))
867            }
868            sym::LIST => {
869                let items: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
870                format!("[{}]", items.join(", "))
871            }
872            sym::REST if n.args.len() == 1 => format!("*{}", self.expr(&n.args[0])),
873            sym::RECORD => {
874                let mut parts = Vec::new();
875                for pair in n.args.chunks(2) {
876                    if pair.len() == 2 {
877                        let k = pair[0]
878                            .as_keyword()
879                            .map(str::to_string)
880                            .unwrap_or_else(|| self.expr(&pair[0]));
881                        parts.push(format!("{k}: {}", self.expr(&pair[1])));
882                    }
883                }
884                format!("{{{}}}", parts.join(", "))
885            }
886            sym::MAP => {
887                let mut parts = Vec::new();
888                for pair in n.args.chunks(2) {
889                    if pair.len() == 2 {
890                        parts.push(format!("{}: {}", self.expr(&pair[0]), self.expr(&pair[1])));
891                    }
892                }
893                format!("{{{}}}", parts.join(", "))
894            }
895            sym::FN if n.args.len() == 2 => {
896                let params = self.params(&n.args[0]);
897                let body = &n.args[1];
898                let b = if body.args.len() == 1 {
899                    self.expr(&body.args[0])
900                } else {
901                    self.expr(body)
902                };
903                format!("lambda {params}: {b}")
904            }
905            sym::QUOTE if n.args.len() == 1 => {
906                // A quoted block prints as `quote:` + body; the statement printer handles the
907                // block-call case before reaching here.
908                format!("quote({})", self.expr(&n.args[0]))
909            }
910            sym::CALL if !n.args.is_empty() => {
911                let callee = self.expr(&n.args[0]);
912                let args: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
913                format!("{callee}({})", args.join(", "))
914            }
915            sym::DO => {
916                let items: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
917                items.join("; ")
918            }
919            _ => {
920                let mut h = String::new();
921                write_atom(&mut h, n);
922                let args: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
923                format!("{h}({})", args.join(", "))
924            }
925        }
926    }
927}
928
929/// Recognise `f(args, do=quote(block))` so it can print back as `f(args):` + block.
930/// A test's session slot, printed back as it was written.
931///
932/// One node with two shapes — a bare actor, or `(at "ana" "/done")` — so this is where the two
933/// meet and every clause that has a session slot prints through it rather than spelling
934/// `session("…")` a fourth time.
935fn session_of(n: &Node) -> Option<String> {
936    if let Some(actor) = n.as_str_lit() {
937        return Some(format!("session({})", quoted(actor)));
938    }
939    if n.is_form(sym::AT) && n.args.len() == 2 {
940        let actor = n.args[0].as_str_lit()?;
941        let route = n.args[1].as_str_lit()?;
942        return Some(format!("session({}, {})", quoted(actor), quoted(route)));
943    }
944    None
945}
946
947fn quoted(s: &str) -> String {
948    let mut out = String::with_capacity(s.len() + 2);
949    write_string(&mut out, s);
950    out
951}
952
953fn split_block_call(n: &Node) -> Option<(&str, Vec<Node>, Node)> {
954    let head = n.head_name()?;
955    let last = n.args.last()?;
956    if !last.is_form(sym::KW_ARG) || last.args.len() != 2 {
957        return None;
958    }
959    if last.args[0].as_var().map(|s| s.as_str()) != Some("do") {
960        return None;
961    }
962    let quoted = &last.args[1];
963    if !quoted.is_form(sym::QUOTE) || quoted.args.len() != 1 {
964        return None;
965    }
966    let block = quoted.args[0].clone();
967    if !block.has_head(sym::DO) {
968        return None;
969    }
970    Some((head, n.args[..n.args.len() - 1].to_vec(), block))
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976    use crate::{parser, sexpr};
977    use beck_diag::{Diagnostics, SourceMap};
978
979    fn roundtrip_python(src: &str) -> String {
980        let mut map = SourceMap::new();
981        let f = map.add("t.beck", src);
982        let mut d = Diagnostics::new();
983        let n = parser::parse_module(f, "t", src, &mut d);
984        assert!(!d.has_errors(), "{}", d.render(&map));
985        to_python(&n)
986    }
987
988    fn parse(src: &str) -> Node {
989        let mut map = SourceMap::new();
990        let f = map.add("t.beck", src);
991        let mut d = Diagnostics::new();
992        let n = parser::parse_module(f, "t", src, &mut d);
993        assert!(!d.has_errors(), "{}", d.render(&map));
994        n
995    }
996
997    #[test]
998    fn printing_python_is_idempotent_and_reparses_to_the_same_tree() {
999        let src = "\
1000def total(items: list[Int], base: Int) -> Int:
1001    var acc = base
1002    for i in items:
1003        acc = (acc + i)
1004    if (acc > 10):
1005        return acc
1006    elif (acc > 5):
1007        return 5
1008    else:
1009        return 0
1010";
1011        let once = roundtrip_python(src);
1012        let twice = roundtrip_python(&once);
1013        assert_eq!(once, twice, "fmt must be idempotent");
1014        assert!(parse(src).structurally_eq(&parse(&once)));
1015    }
1016
1017    #[test]
1018    fn block_calls_print_back_as_blocks() {
1019        let src = "ui:\n    main:\n        h1(class=\"t\"):\n            \"todos\"\n";
1020        let out = roundtrip_python(src);
1021        assert!(out.contains("ui:"), "{out}");
1022        assert!(out.contains("h1(class=\"t\"):"), "{out}");
1023        assert!(parse(src).structurally_eq(&parse(&out)));
1024    }
1025
1026    #[test]
1027    fn the_two_surfaces_are_the_same_language() {
1028        // §2.2's claim, mechanised: both readers produce identical `Node` trees.
1029        let py = "def toggle(todos: Map[Id, Todo], e: Toggled) -> Map[Id, Todo]:\n\
1030                  \x20   return todos.update(e.id, lambda t: t.with(done=not t.done))\n";
1031        let mut map = SourceMap::new();
1032        let f = map.add("t.beck", py);
1033        let mut d = Diagnostics::new();
1034        let from_py = parser::parse_module(f, "t", py, &mut d);
1035        assert!(!d.has_errors(), "{}", d.render(&map));
1036
1037        let sx_src = "(def toggle
1038                        (params (: todos (Map Id Todo)) (: e Toggled))
1039                        (returns (Map Id Todo))
1040                        (uses)
1041                        (do (return (. todos update (. e id)
1042                             (fn (params t) (do (. t with (kw done (not (. t done)))))))))) ";
1043        let g = map.add("t.sx", sx_src);
1044        let from_sx = sexpr::read_one(g, sx_src, &mut d).unwrap();
1045        assert!(!d.has_errors(), "{}", d.render(&map));
1046
1047        assert!(
1048            from_py.args[1].structurally_eq(&from_sx),
1049            "python:\n{}\nsexpr:\n{}",
1050            to_sexpr(&from_py.args[1]),
1051            to_sexpr(&from_sx)
1052        );
1053    }
1054
1055    #[test]
1056    fn sexpr_pretty_breaks_only_long_forms() {
1057        let n = parse("def f() -> Int:\n    return 1\n");
1058        let pretty = to_sexpr_pretty(&n.args[1]);
1059        assert_eq!(pretty.lines().count(), 1, "{pretty}");
1060    }
1061}
1062
1063/// The round-trip property, over the corpus.
1064///
1065/// §4.8: "Round-trip property: `parse(print(parse(src))) == parse(src)`". It is a property of the
1066/// *printer* rather than of any one construct, so it is asserted over whole files — the example
1067/// program is the corpus Phase 1 has, and it exercises every surface form the language ships.
1068#[cfg(test)]
1069mod roundtrip {
1070    use super::*;
1071    use crate::parser;
1072    use beck_diag::{Diagnostics, SourceMap};
1073
1074    fn parse(name: &str, src: &str) -> Node {
1075        let mut map = SourceMap::new();
1076        let f = map.add(name, src);
1077        let mut d = Diagnostics::new();
1078        let n = parser::parse_module(f, "t", src, &mut d);
1079        assert!(!d.has_errors(), "{name}:\n{}", d.render(&map));
1080        n
1081    }
1082
1083    fn corpus() -> Vec<(&'static str, &'static str)> {
1084        vec![
1085            ("example", include_str!("../../../examples/todo.beck")),
1086            (
1087                "macros",
1088                "macro unless(cond, do):\n    return quote:\n        if not $cond:\n            $do\n",
1089            ),
1090            (
1091                "control",
1092                "def f(xs: list[Int]) -> Int:\n    var acc = 0\n    if (acc > 1):\n        return 1\n    elif (acc > 0):\n        return 2\n    else:\n        return 3\n",
1093            ),
1094            (
1095                "types",
1096                "type Id = newtype[Str]\n\nmodel M:\n    a: Int\n\nunion U:\n    A(x: Int)\n    B\n",
1097            ),
1098            // §21.2's clauses are part of the surface now, so they are part of the property that
1099            // says the surface is a fixed point of printing.
1100            (
1101                "tests",
1102                "test \"a\":\n    given [Added(id=\"1\")] by \"ana\"\n    when session(\"ana\") sends Add(id=\"1\"), Toggle(id=\"1\")\n    stub net.out(payments.example.com): Declined\n    stub net.out(a.example.com):\n        case Charge(amount):\n            return Declined\n        case _:\n            return Approved\n    stub net.out(b.example.com):\n        x = 1\n        return Approved\n    expect page contains \"milk\"\n    expect page(session(\"bo\")) contains \"milk\"\n    expect state == fold_of []\n    expect state == fold_of [Added(id=\"1\")] by \"ana\"\n    expect place(view) == client\n    expect flow(ApiKey) reaches nothing on client\n    expect wire_compatible_with \"o.becki\"\n    expect no net.out\n    expect net.out(h.example.com) once\n    expect net.out(h.example.com) times 3\n    expect net.out(h.example.com) with Charge(amount=1)\n    expect Err(error=BlankText)\n\nproperty \"p\"(events: list[Event]):\n    given events\n    expect list_len(events) >= 0\n",
1103            ),
1104        ]
1105    }
1106
1107    #[test]
1108    fn printing_the_python_surface_round_trips() {
1109        for (name, src) in corpus() {
1110            let first = parse(name, src);
1111            let printed = to_python(&first);
1112            let leaked: &'static str = Box::leak(printed.clone().into_boxed_str());
1113            let second = parse(name, leaked);
1114            assert!(
1115                first.structurally_eq(&second),
1116                "{name} did not round-trip.\n--- printed ---\n{printed}\n--- as sexpr ---\n{}\n--- was ---\n{}",
1117                to_sexpr(&second),
1118                to_sexpr(&first)
1119            );
1120        }
1121    }
1122
1123    #[test]
1124    fn formatting_is_idempotent() {
1125        for (name, src) in corpus() {
1126            let once = to_python(&parse(name, src));
1127            let leaked: &'static str = Box::leak(once.clone().into_boxed_str());
1128            let twice = to_python(&parse(name, leaked));
1129            assert_eq!(once, twice, "{name}: fmt is not idempotent");
1130        }
1131    }
1132}