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    };
130    if n.is_form(sym::MODULE) {
131        for (i, item) in n.args.iter().skip(1).enumerate() {
132            if i > 0 {
133                p.out.push('\n');
134            }
135            p.item(item);
136        }
137    } else {
138        p.item(n);
139    }
140    p.out
141}
142
143struct Py {
144    out: String,
145    indent: usize,
146}
147
148impl Py {
149    fn line(&mut self, s: &str) {
150        for _ in 0..self.indent {
151            self.out.push_str("    ");
152        }
153        self.out.push_str(s);
154        self.out.push('\n');
155    }
156
157    /// Emit the node's doc comment, if it has one, at the current indentation.
158    ///
159    /// Ordinary comments are dropped by the lexer and so cannot survive `beck fmt`; a doc comment
160    /// is [`crate::Meta`], so it can, and formatting a documented module has to give it back.
161    fn docs(&mut self, n: &Node) {
162        let Some(doc) = n.meta.doc.clone() else {
163            return;
164        };
165        let indent = "    ".repeat(self.indent);
166        self.out
167            .push_str(&crate::doc::render(&doc, crate::doc::PY_MARKER, &indent));
168    }
169
170    fn item(&mut self, n: &Node) {
171        self.docs(n);
172        match n.head_name() {
173            Some(sym::DECORATE) => {
174                let deco = self.expr(&n.args[0]);
175                self.line(&format!("@{deco}"));
176                self.item(&n.args[1]);
177            }
178            Some(sym::DEF) => self.def(n),
179            Some(sym::MACRO) => {
180                let name = self.expr(&n.args[0]);
181                let params = self.params(&n.args[1]);
182                self.line(&format!("macro {name}({params}):"));
183                self.body(&n.args[2]);
184            }
185            Some(sym::MODEL) => {
186                let name = self.expr(&n.args[0]);
187                let typarams = self.typarams(n);
188                self.line(&format!("model {name}{typarams}:"));
189                self.indent += 1;
190                if n.args.len() == 2 {
191                    self.line("pass");
192                }
193                for f in &n.args[2..] {
194                    self.docs(f);
195                    let fname = self.expr(&f.args[0]);
196                    let fty = self.type_expr(&f.args[1]);
197                    self.line(&format!("{fname}: {fty}"));
198                }
199                self.indent -= 1;
200            }
201            Some(sym::UNION) => {
202                let name = self.expr(&n.args[0]);
203                let typarams = self.typarams(n);
204                self.line(&format!("union {name}{typarams}:"));
205                self.indent += 1;
206                for v in &n.args[2..] {
207                    self.docs(v);
208                    let vname = self.expr(&v.args[0]);
209                    if v.args.len() == 1 {
210                        self.line(&vname);
211                    } else {
212                        let fields: Vec<String> = v.args[1..]
213                            .iter()
214                            .map(|f| {
215                                format!("{}: {}", self.expr(&f.args[0]), self.type_expr(&f.args[1]))
216                            })
217                            .collect();
218                        self.line(&format!("{vname}({})", fields.join(", ")));
219                    }
220                }
221                self.indent -= 1;
222            }
223            Some(sym::TRAIT) => {
224                let name = self.expr(&n.args[0]);
225                self.line(&format!("trait {name}:"));
226                self.indent += 1;
227                for m in &n.args[1..] {
228                    self.item(m);
229                }
230                self.indent -= 1;
231            }
232            Some(sym::IMPL) => {
233                let name = self.expr(&n.args[0]);
234                let typarams = self.typarams(n);
235                let ty = self.type_expr(&n.args[2]);
236                self.line(&format!("impl{typarams} {name} for {ty}:"));
237                self.indent += 1;
238                for m in &n.args[3..] {
239                    self.item(m);
240                }
241                self.indent -= 1;
242            }
243            Some(sym::TYPE) => {
244                let name = self.expr(&n.args[0]);
245                let typarams = self.typarams(n);
246                let ty = self.type_expr(&n.args[2]);
247                self.line(&format!("type {name}{typarams} = {ty}"));
248            }
249            Some(sym::NEWTYPE) => {
250                let name = self.expr(&n.args[0]);
251                let typarams = self.typarams(n);
252                let ty = self.type_expr(&n.args[2]);
253                self.line(&format!("type {name}{typarams} = newtype[{ty}]"));
254            }
255            Some(sym::IMPORT) => {
256                let path = self.expr(&n.args[0]);
257                self.line(&format!("import {path}"));
258            }
259            Some(sym::ROW) if n.args.len() >= 2 => {
260                let name = self.expr(&n.args[0]);
261                let atoms: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
262                self.line(&format!("row {name} = {}", atoms.join(", ")));
263            }
264            Some(sym::IDENTITY) if n.args.len() == 1 => {
265                let provider = self.expr(&n.args[0]);
266                self.line(&format!("identity = {provider}"));
267            }
268            Some(sym::TEST) => {
269                let name = self.expr(&n.args[0]);
270                self.line(&format!("test {name}:"));
271                self.body(&n.args[1]);
272            }
273            Some(sym::PROPERTY) if n.args.len() == 3 => {
274                let name = self.expr(&n.args[0]);
275                let params = self.params(&n.args[1]);
276                self.line(&format!("property {name}({params}):"));
277                self.body(&n.args[2]);
278            }
279            _ => self.stmt(n),
280        }
281    }
282
283    /// `[T, U]` or `[T: Show + Eq]` from the list at `args[1]`, or the empty string when there is
284    /// nothing to quantify.
285    fn typarams(&mut self, n: &Node) -> String {
286        let Some(t) = n.args.get(1).filter(|t| !t.args.is_empty()) else {
287            return String::new();
288        };
289        let names: Vec<String> = t
290            .args
291            .clone()
292            .iter()
293            .map(|a| {
294                if !a.is_form(sym::ANNOT) || a.args.len() < 2 {
295                    return self.expr(a);
296                }
297                let bounds: Vec<String> = a.args[1..].iter().map(|b| self.expr(b)).collect();
298                format!("{}: {}", self.expr(&a.args[0]), bounds.join(" + "))
299            })
300            .collect();
301        format!("[{}]", names.join(", "))
302    }
303
304    fn def(&mut self, n: &Node) {
305        let name = self.expr(&n.args[0]);
306        let typarams = self.typarams(n);
307        let params = self.params(&n.args[2]);
308        let ret = n
309            .args
310            .get(3)
311            .filter(|r| !r.args.is_empty())
312            .map(|r| format!(" -> {}", self.type_expr(&r.args[0])))
313            .unwrap_or_default();
314        let uses = n
315            .args
316            .get(4)
317            .filter(|u| !u.args.is_empty())
318            .map(|u| {
319                let items: Vec<String> = u.args.iter().map(|e| self.expr(e)).collect();
320                format!(" uses {}", items.join(", "))
321            })
322            .unwrap_or_default();
323        match n.args.get(5) {
324            Some(body) => {
325                self.line(&format!("def {name}{typarams}({params}){ret}{uses}:"));
326                self.body(body);
327            }
328            // A declaration: a trait's method signature, or a line of a `.becki` interface (§3.6).
329            // It prints without a colon, which is what it parses back from.
330            None => self.line(&format!("def {name}{typarams}({params}){ret}{uses}")),
331        }
332    }
333
334    fn params(&self, n: &Node) -> String {
335        n.args
336            .iter()
337            .map(|p| {
338                if p.is_form(sym::ANNOT) {
339                    format!("{}: {}", self.expr(&p.args[0]), self.type_expr(&p.args[1]))
340                } else {
341                    self.expr(p)
342                }
343            })
344            .collect::<Vec<_>>()
345            .join(", ")
346    }
347
348    /// The pattern of a `case`, and its guard when it has one.
349    fn case_head(&self, arm: &Node) -> String {
350        let pat = self.expr(&arm.args[0]);
351        match arm.args.get(2) {
352            Some(g) => format!("{pat} if {}", self.expr(g)),
353            None => pat,
354        }
355    }
356
357    fn body(&mut self, n: &Node) {
358        self.indent += 1;
359        if n.args.is_empty() {
360            self.line("pass");
361        }
362        for s in &n.args {
363            self.stmt(s);
364        }
365        self.indent -= 1;
366    }
367
368    fn stmt(&mut self, n: &Node) {
369        match n.head_name() {
370            Some(sym::DO) => {
371                for s in &n.args {
372                    self.stmt(s);
373                }
374            }
375            Some(sym::RETURN) => match n.args.first() {
376                // `return ui:` + block. The block rule applies in final position (§2.7 only
377                // forbids a block-form call as a *non-final argument*), so the printer has to
378                // reproduce it — printing `return ui(do=quote(...))` would not re-parse, because
379                // `quote` is a block form and `;`-joined statements are not surface syntax.
380                // `return quote:` + template — the shape every macro body has (§2.4).
381                // `return try:` + block, for the same reason as the `quote` case below it: the
382                // handler carries an indented body, and `try((do …))` is not surface syntax.
383                Some(e) if e.is_form(sym::TRY) && e.args.len() == 1 => {
384                    self.line("return try:");
385                    self.body(&e.args[0]);
386                }
387                Some(e) if e.is_form(sym::PARALLEL) && e.args.len() == 1 => {
388                    self.line("return parallel:");
389                    self.body(&e.args[0]);
390                }
391                Some(e) if e.is_form(sym::QUOTE) && e.args.len() == 1 => {
392                    self.line("return quote:");
393                    let body = &e.args[0];
394                    if body.is_form(sym::DO) {
395                        self.body(body);
396                    } else {
397                        self.indent += 1;
398                        self.stmt(body);
399                        self.indent -= 1;
400                    }
401                }
402                Some(e) => match split_block_call(e) {
403                    Some((head, args, block)) => {
404                        let rendered = self.call_text(head, &args);
405                        self.line(&format!("return {rendered}:"));
406                        self.body(&block);
407                    }
408                    None => {
409                        let e = self.expr(e);
410                        self.line(&format!("return {e}"));
411                    }
412                },
413                None => self.line("return"),
414            },
415            Some(sym::LET) | Some(sym::VAR)
416                if n.args.len() == 2 && split_block_call(&n.args[1]).is_some() =>
417            {
418                let (head, args, block) =
419                    split_block_call(&n.args[1]).expect("checked by the guard");
420                let target = self.expr(&n.args[0]);
421                let keyword = if n.is_form(sym::VAR) { "var " } else { "" };
422                let rendered = self.call_text(head, &args);
423                self.line(&format!("{keyword}{target} = {rendered}:"));
424                self.body(&block);
425            }
426            // `x = try:` and `x = parallel:` — a block form bound to a name. Neither is a call, so
427            // `split_block_call` above cannot see it, and printing `x = try((do …))` would not
428            // re-parse. Both are expressions (`docs/45` §45.2), so both can appear here.
429            Some(sym::LET) | Some(sym::VAR)
430                if n.args.len() == 2
431                    && (n.args[1].is_form(sym::TRY) || n.args[1].is_form(sym::PARALLEL))
432                    && n.args[1].args.len() == 1 =>
433            {
434                let keyword = if n.is_form(sym::VAR) { "var " } else { "" };
435                let target = self.expr(&n.args[0]);
436                let head = n.args[1].head_name().unwrap_or(sym::TRY);
437                self.line(&format!("{keyword}{target} = {head}:"));
438                self.body(&n.args[1].args[0]);
439            }
440            Some(sym::LET) if n.args.len() == 2 => {
441                let t = &n.args[0];
442                let target = if t.is_form(sym::ANNOT) {
443                    format!("{}: {}", self.expr(&t.args[0]), self.type_expr(&t.args[1]))
444                } else {
445                    self.expr(t)
446                };
447                let v = self.expr(&n.args[1]);
448                self.line(&format!("{target} = {v}"));
449            }
450            Some(sym::VAR) if n.args.len() == 2 => {
451                let t = &n.args[0];
452                let target = if t.is_form(sym::ANNOT) {
453                    format!("{}: {}", self.expr(&t.args[0]), self.type_expr(&t.args[1]))
454                } else {
455                    self.expr(t)
456                };
457                let v = self.expr(&n.args[1]);
458                self.line(&format!("var {target} = {v}"));
459            }
460            Some(sym::IF) if n.args.len() >= 2 && n.args[1].is_form(sym::DO) => {
461                let c = self.expr(&n.args[0]);
462                self.line(&format!("if {c}:"));
463                self.body(&n.args[1]);
464                if let Some(alt) = n.args.get(2) {
465                    // `elif` is an `else` whose only statement is another `if`.
466                    if alt.args.len() == 1 && alt.args[0].is_form(sym::IF) {
467                        let inner = &alt.args[0];
468                        let mut s = String::new();
469                        std::mem::swap(&mut self.out, &mut s);
470                        self.stmt(inner);
471                        std::mem::swap(&mut self.out, &mut s);
472                        let pad = "    ".repeat(self.indent);
473                        let rewritten = s.replacen(&format!("{pad}if "), &format!("{pad}elif "), 1);
474                        self.out.push_str(&rewritten);
475                    } else {
476                        self.line("else:");
477                        self.body(alt);
478                    }
479                }
480            }
481            Some(sym::FOR) if n.args.len() == 3 => {
482                let v = self.expr(&n.args[0]);
483                let seq = self.expr(&n.args[1]);
484                self.line(&format!("for {v} in {seq}:"));
485                self.body(&n.args[2]);
486            }
487            Some(sym::WHILE) if n.args.len() == 2 => {
488                let c = self.expr(&n.args[0]);
489                self.line(&format!("while {c}:"));
490                self.body(&n.args[1]);
491            }
492            Some(sym::MATCH) if !n.args.is_empty() => {
493                let s = self.expr(&n.args[0]);
494                self.line(&format!("match {s}:"));
495                self.indent += 1;
496                for arm in &n.args[1..] {
497                    self.line(&format!("case {}:", self.case_head(arm)));
498                    self.body(&arm.args[1]);
499                }
500                self.indent -= 1;
501            }
502            // `try:` + block. Like `ui:` it carries an indented body, so it prints as one rather
503            // than as a call — `try((do …))` is not surface syntax and would not re-parse.
504            Some(sym::TRY) if n.args.len() == 1 => {
505                self.line("try:");
506                self.body(&n.args[0]);
507            }
508            // `parallel:` + block, for the same reason: the scope's children *are* the indented
509            // statements, so there is no call form to print it as.
510            Some(sym::PARALLEL) if n.args.len() == 1 => {
511                self.line("parallel:");
512                self.body(&n.args[0]);
513            }
514            Some(sym::ROW | sym::IDENTITY) => self.item(n),
515            Some(sym::DEF | sym::MACRO | sym::MODEL | sym::UNION | sym::TYPE | sym::NEWTYPE)
516            | Some(sym::TRAIT | sym::IMPL | sym::IMPORT | sym::DECORATE | sym::TEST)
517            | Some(sym::PROPERTY) => self.item(n),
518
519            // ---- §21.2's clauses. Each prints back as the line it was written as, because
520            // `parse(print(parse(src))) == parse(src)` is asserted over the corpus and a test block
521            // is part of the corpus now.
522            Some(sym::GIVEN) if !n.args.is_empty() => {
523                let events = self.expr(&n.args[0]);
524                match n.args.get(1) {
525                    Some(actor) => {
526                        let a = self.expr(actor);
527                        self.line(&format!("given {events} by {a}"));
528                    }
529                    None => self.line(&format!("given {events}")),
530                }
531            }
532            Some(sym::WHEN) if n.args.len() >= 2 => {
533                let cmds: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
534                let cmds = cmds.join(", ");
535                match n.args[0].as_str_lit() {
536                    Some(actor) => self.line(&format!("when session(\"{actor}\") sends {cmds}")),
537                    None => self.line(&format!("when {cmds}")),
538                }
539            }
540            Some(sym::EXPECT) if n.args.len() == 1 => {
541                // `expect Ok(…)`/`expect Err(…)` parsed as `result == …`; printing the desugared
542                // form is what makes the round-trip a fixed point rather than an oscillation.
543                let e = self.expr(&n.args[0]);
544                self.line(&format!("expect {e}"));
545            }
546            Some(sym::EXPECT_CONTAINS) if !n.args.is_empty() => {
547                let needle = self.expr(&n.args[0]);
548                match n.args.get(1).and_then(|a| a.as_str_lit()) {
549                    Some(actor) => self.line(&format!(
550                        "expect page(session(\"{actor}\")) contains {needle}"
551                    )),
552                    None => self.line(&format!("expect page contains {needle}")),
553                }
554            }
555            Some(sym::EXPECT_SNAPSHOT) if n.args.len() == 2 => {
556                let subject = match n.args[1].as_str_lit() {
557                    Some(actor) => format!("page(session(\"{actor}\"))"),
558                    None => "page".to_string(),
559                };
560                match n.args[0].as_str_lit() {
561                    Some(name) => {
562                        self.line(&format!("expect {subject} matches snapshot \"{name}\""))
563                    }
564                    None => self.line(&format!("expect {subject} matches snapshot")),
565                }
566            }
567            Some(sym::EXPECT_FOLD) if !n.args.is_empty() => {
568                let events = self.expr(&n.args[0]);
569                match n.args.get(1).and_then(|a| a.as_str_lit()) {
570                    Some(actor) => {
571                        self.line(&format!("expect state == fold_of {events} by \"{actor}\""))
572                    }
573                    None => self.line(&format!("expect state == fold_of {events}")),
574                }
575            }
576            Some(sym::EXPECT_PLACE) if n.args.len() == 2 => {
577                let what = self.expr(&n.args[0]);
578                let tier = self.expr(&n.args[1]);
579                self.line(&format!("expect place({what}) == {tier}"));
580            }
581            Some(sym::EXPECT_FLOW) if n.args.len() == 2 => {
582                let ty = self.expr(&n.args[0]);
583                let tier = self.expr(&n.args[1]);
584                self.line(&format!("expect flow({ty}) reaches nothing on {tier}"));
585            }
586            Some(sym::EXPECT_WIRE) if n.args.len() == 1 => {
587                let path = self.expr(&n.args[0]);
588                self.line(&format!("expect wire_compatible_with {path}"));
589            }
590            Some(sym::EXPECT_EFFECT) if n.args.len() == 2 => {
591                let atom = n.args[0].as_str_lit().unwrap_or_default().to_string();
592                let how = &n.args[1];
593                match how.head_name() {
594                    Some("times") if how.args.len() == 1 => {
595                        match how.args[0].as_lit() {
596                            Some(Lit::Int(1)) => self.line(&format!("expect {atom} once")),
597                            Some(Lit::Int(k)) => self.line(&format!("expect {atom} times {k}")),
598                            _ => self.line(&format!("expect {atom} once")),
599                        };
600                    }
601                    Some("with") if how.args.len() == 1 => {
602                        let v = self.expr(&how.args[0]);
603                        self.line(&format!("expect {atom} with {v}"));
604                    }
605                    _ => self.line(&format!("expect no {atom}")),
606                }
607            }
608            Some(sym::STUB) if n.args.len() == 2 => {
609                let atom = n.args[0].as_str_lit().unwrap_or_default().to_string();
610                let body = &n.args[1];
611                if body.is_form(sym::STUB_ARMS) {
612                    self.line(&format!("stub {atom}:"));
613                    self.indent += 1;
614                    for arm in &body.args {
615                        self.line(&format!("case {}:", self.case_head(arm)));
616                        self.body(&arm.args[1]);
617                    }
618                    self.indent -= 1;
619                } else if body.is_form(sym::DO) {
620                    self.line(&format!("stub {atom}:"));
621                    self.body(body);
622                } else {
623                    let v = self.expr(body);
624                    self.line(&format!("stub {atom}: {v}"));
625                }
626            }
627            _ => {
628                // A call with a `do=` block prints back in block form; anything else is an
629                // expression statement.
630                if let Some((head, args, block)) = split_block_call(n) {
631                    let rendered = self.call_text(head, &args);
632                    self.line(&format!("{rendered}:"));
633                    self.body(&block);
634                } else {
635                    let e = self.expr(n);
636                    self.line(&e);
637                }
638            }
639        }
640    }
641
642    fn call_text(&self, head: &str, args: &[Node]) -> String {
643        if args.is_empty() {
644            return head.to_string();
645        }
646        let rendered: Vec<String> = args.iter().map(|a| self.expr(a)).collect();
647        format!("{head}({})", rendered.join(", "))
648    }
649
650    fn type_expr(&self, n: &Node) -> String {
651        match n.head_name() {
652            // `>= 1`, not `>= 2`: a `fn-type` node is its parameters followed by its result, so a
653            // function type taking **no** arguments has exactly one. `docs/63` §63.3 found `() -> T`
654            // missing from the parser and the checker; the printer kept the same off-by-one, and
655            // printed `fn-type[T]` — the internal head, in a file `beck fmt` had just written.
656            Some("fn-type") if !n.args.is_empty() => {
657                let params: Vec<String> = n.args[..n.args.len() - 1]
658                    .iter()
659                    .map(|a| self.type_expr(a))
660                    .collect();
661                format!(
662                    "({}) -> {}",
663                    params.join(", "),
664                    self.type_expr(&n.args[n.args.len() - 1])
665                )
666            }
667            _ if !n.applied => {
668                let mut s = String::new();
669                write_atom(&mut s, n);
670                s
671            }
672            _ => {
673                let mut head = String::new();
674                write_atom(&mut head, n);
675                let args: Vec<String> = n.args.iter().map(|a| self.type_expr(a)).collect();
676                format!("{head}[{}]", args.join(", "))
677            }
678        }
679    }
680
681    /// The body of a block form, as one line.
682    ///
683    /// A `do` wrapping a single statement is §2.3's single-line block, which is what every block
684    /// form written as an operand is. A `do` with several is not expressible inline — the surface
685    /// has no separator for statements — so it is printed as its statements joined by `; `, which
686    /// does not re-parse and is why `beck-cli/tests/roundtrip.rs` would fail on one. Nothing in
687    /// this tree writes that shape; a program that does is a gap in the surface rather than in the
688    /// printer, and the failing test is where that argument gets had.
689    fn block_expr(&self, n: &Node) -> String {
690        if n.is_form(sym::DO) {
691            let parts: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
692            return parts.join("; ");
693        }
694        self.expr(n)
695    }
696
697    fn expr(&self, n: &Node) -> String {
698        if !n.applied {
699            let mut s = String::new();
700            write_atom(&mut s, n);
701            return s;
702        }
703        let head = n.head_name().unwrap_or("");
704        match head {
705            "not" if n.args.len() == 1 => format!("not {}", self.expr(&n.args[0])),
706            sym::RAISE if n.args.len() == 1 => format!("raise {}", self.expr(&n.args[0])),
707            // `try:` and `parallel:` are **expressions** (`docs/45` §45.2), so they turn up as
708            // operands — `expect (try: benchmark()) == Ok(True)` is how seven files in this tree
709            // assert a fallible answer. §2.3's single-line block form is the notation for one here,
710            // and the parentheses are what let it be an operand at all. Without this they printed
711            // as `try(…)`, which is not surface syntax: `beck fmt` emitted a program that does not
712            // compile, in ten files, until `beck-cli/tests/roundtrip.rs` existed to say so.
713            sym::TRY | sym::PARALLEL if n.args.len() == 1 => {
714                format!("({head}: {})", self.block_expr(&n.args[0]))
715            }
716            "negate" if n.args.len() == 1 => format!("-{}", self.expr(&n.args[0])),
717            sym::UNQUOTE if n.args.len() == 1 => format!("${}", self.expr(&n.args[0])),
718            sym::SPLICE if n.args.len() == 1 => format!("$*{}", self.expr(&n.args[0])),
719            "and" | "or" | "|" | "@" | "==" | "!=" | "<" | "<=" | ">" | ">=" | "+" | "-" | "*"
720            | "/" | "%"
721                if n.args.len() == 2 =>
722            {
723                format!(
724                    "({} {head} {})",
725                    self.expr(&n.args[0]),
726                    self.expr(&n.args[1])
727                )
728            }
729            "contains" if n.args.len() == 2 => {
730                format!("({} in {})", self.expr(&n.args[0]), self.expr(&n.args[1]))
731            }
732            "index" if n.args.len() == 2 => {
733                format!("{}[{}]", self.expr(&n.args[0]), self.expr(&n.args[1]))
734            }
735            // Parenthesised, like every binary operator above it and for the same reason: without
736            // them `a + b if c else d` reads back as `a + (b if c else d)`, which is a different
737            // program that still parses. `clbg/fannkuchredux.beck` is where that showed up.
738            sym::IF if n.args.len() == 3 && !n.args[1].is_form(sym::DO) => format!(
739                "({} if {} else {})",
740                self.expr(&n.args[1]),
741                self.expr(&n.args[0]),
742                self.expr(&n.args[2])
743            ),
744            sym::DOT if n.args.len() == 2 => {
745                format!("{}.{}", self.expr(&n.args[0]), self.expr(&n.args[1]))
746            }
747            sym::DOT if n.args.len() > 2 => {
748                let args: Vec<String> = n.args[2..].iter().map(|a| self.expr(a)).collect();
749                format!(
750                    "{}.{}({})",
751                    self.expr(&n.args[0]),
752                    self.expr(&n.args[1]),
753                    args.join(", ")
754                )
755            }
756            sym::KW_ARG if n.args.len() == 2 => {
757                format!("{}={}", self.expr(&n.args[0]), self.expr(&n.args[1]))
758            }
759            sym::LIST => {
760                let items: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
761                format!("[{}]", items.join(", "))
762            }
763            sym::REST if n.args.len() == 1 => format!("*{}", self.expr(&n.args[0])),
764            sym::RECORD => {
765                let mut parts = Vec::new();
766                for pair in n.args.chunks(2) {
767                    if pair.len() == 2 {
768                        let k = pair[0]
769                            .as_keyword()
770                            .map(str::to_string)
771                            .unwrap_or_else(|| self.expr(&pair[0]));
772                        parts.push(format!("{k}: {}", self.expr(&pair[1])));
773                    }
774                }
775                format!("{{{}}}", parts.join(", "))
776            }
777            sym::MAP => {
778                let mut parts = Vec::new();
779                for pair in n.args.chunks(2) {
780                    if pair.len() == 2 {
781                        parts.push(format!("{}: {}", self.expr(&pair[0]), self.expr(&pair[1])));
782                    }
783                }
784                format!("{{{}}}", parts.join(", "))
785            }
786            sym::FN if n.args.len() == 2 => {
787                let params = self.params(&n.args[0]);
788                let body = &n.args[1];
789                let b = if body.args.len() == 1 {
790                    self.expr(&body.args[0])
791                } else {
792                    self.expr(body)
793                };
794                format!("lambda {params}: {b}")
795            }
796            sym::QUOTE if n.args.len() == 1 => {
797                // A quoted block prints as `quote:` + body; the statement printer handles the
798                // block-call case before reaching here.
799                format!("quote({})", self.expr(&n.args[0]))
800            }
801            sym::CALL if !n.args.is_empty() => {
802                let callee = self.expr(&n.args[0]);
803                let args: Vec<String> = n.args[1..].iter().map(|a| self.expr(a)).collect();
804                format!("{callee}({})", args.join(", "))
805            }
806            sym::DO => {
807                let items: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
808                items.join("; ")
809            }
810            _ => {
811                let mut h = String::new();
812                write_atom(&mut h, n);
813                let args: Vec<String> = n.args.iter().map(|a| self.expr(a)).collect();
814                format!("{h}({})", args.join(", "))
815            }
816        }
817    }
818}
819
820/// Recognise `f(args, do=quote(block))` so it can print back as `f(args):` + block.
821fn split_block_call(n: &Node) -> Option<(&str, Vec<Node>, Node)> {
822    let head = n.head_name()?;
823    let last = n.args.last()?;
824    if !last.is_form(sym::KW_ARG) || last.args.len() != 2 {
825        return None;
826    }
827    if last.args[0].as_var().map(|s| s.as_str()) != Some("do") {
828        return None;
829    }
830    let quoted = &last.args[1];
831    if !quoted.is_form(sym::QUOTE) || quoted.args.len() != 1 {
832        return None;
833    }
834    let block = quoted.args[0].clone();
835    if !block.has_head(sym::DO) {
836        return None;
837    }
838    Some((head, n.args[..n.args.len() - 1].to_vec(), block))
839}
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844    use crate::{parser, sexpr};
845    use beck_diag::{Diagnostics, SourceMap};
846
847    fn roundtrip_python(src: &str) -> String {
848        let mut map = SourceMap::new();
849        let f = map.add("t.beck", src);
850        let mut d = Diagnostics::new();
851        let n = parser::parse_module(f, "t", src, &mut d);
852        assert!(!d.has_errors(), "{}", d.render(&map));
853        to_python(&n)
854    }
855
856    fn parse(src: &str) -> Node {
857        let mut map = SourceMap::new();
858        let f = map.add("t.beck", src);
859        let mut d = Diagnostics::new();
860        let n = parser::parse_module(f, "t", src, &mut d);
861        assert!(!d.has_errors(), "{}", d.render(&map));
862        n
863    }
864
865    #[test]
866    fn printing_python_is_idempotent_and_reparses_to_the_same_tree() {
867        let src = "\
868def total(items: list[Int], base: Int) -> Int:
869    var acc = base
870    for i in items:
871        acc = (acc + i)
872    if (acc > 10):
873        return acc
874    elif (acc > 5):
875        return 5
876    else:
877        return 0
878";
879        let once = roundtrip_python(src);
880        let twice = roundtrip_python(&once);
881        assert_eq!(once, twice, "fmt must be idempotent");
882        assert!(parse(src).structurally_eq(&parse(&once)));
883    }
884
885    #[test]
886    fn block_calls_print_back_as_blocks() {
887        let src = "ui:\n    main:\n        h1(class=\"t\"):\n            \"todos\"\n";
888        let out = roundtrip_python(src);
889        assert!(out.contains("ui:"), "{out}");
890        assert!(out.contains("h1(class=\"t\"):"), "{out}");
891        assert!(parse(src).structurally_eq(&parse(&out)));
892    }
893
894    #[test]
895    fn the_two_surfaces_are_the_same_language() {
896        // §2.2's claim, mechanised: both readers produce identical `Node` trees.
897        let py = "def toggle(todos: Map[Id, Todo], e: Toggled) -> Map[Id, Todo]:\n\
898                  \x20   return todos.update(e.id, lambda t: t.with(done=not t.done))\n";
899        let mut map = SourceMap::new();
900        let f = map.add("t.beck", py);
901        let mut d = Diagnostics::new();
902        let from_py = parser::parse_module(f, "t", py, &mut d);
903        assert!(!d.has_errors(), "{}", d.render(&map));
904
905        let sx_src = "(def toggle
906                        (params (: todos (Map Id Todo)) (: e Toggled))
907                        (returns (Map Id Todo))
908                        (uses)
909                        (do (return (. todos update (. e id)
910                             (fn (params t) (do (. t with (kw done (not (. t done)))))))))) ";
911        let g = map.add("t.sx", sx_src);
912        let from_sx = sexpr::read_one(g, sx_src, &mut d).unwrap();
913        assert!(!d.has_errors(), "{}", d.render(&map));
914
915        assert!(
916            from_py.args[1].structurally_eq(&from_sx),
917            "python:\n{}\nsexpr:\n{}",
918            to_sexpr(&from_py.args[1]),
919            to_sexpr(&from_sx)
920        );
921    }
922
923    #[test]
924    fn sexpr_pretty_breaks_only_long_forms() {
925        let n = parse("def f() -> Int:\n    return 1\n");
926        let pretty = to_sexpr_pretty(&n.args[1]);
927        assert_eq!(pretty.lines().count(), 1, "{pretty}");
928    }
929}
930
931/// The round-trip property, over the corpus.
932///
933/// §4.8: "Round-trip property: `parse(print(parse(src))) == parse(src)`". It is a property of the
934/// *printer* rather than of any one construct, so it is asserted over whole files — the example
935/// program is the corpus Phase 1 has, and it exercises every surface form the language ships.
936#[cfg(test)]
937mod roundtrip {
938    use super::*;
939    use crate::parser;
940    use beck_diag::{Diagnostics, SourceMap};
941
942    fn parse(name: &str, src: &str) -> Node {
943        let mut map = SourceMap::new();
944        let f = map.add(name, src);
945        let mut d = Diagnostics::new();
946        let n = parser::parse_module(f, "t", src, &mut d);
947        assert!(!d.has_errors(), "{name}:\n{}", d.render(&map));
948        n
949    }
950
951    fn corpus() -> Vec<(&'static str, &'static str)> {
952        vec![
953            ("example", include_str!("../../../examples/todo.beck")),
954            (
955                "macros",
956                "macro unless(cond, do):\n    return quote:\n        if not $cond:\n            $do\n",
957            ),
958            (
959                "control",
960                "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",
961            ),
962            (
963                "types",
964                "type Id = newtype[Str]\n\nmodel M:\n    a: Int\n\nunion U:\n    A(x: Int)\n    B\n",
965            ),
966            // §21.2's clauses are part of the surface now, so they are part of the property that
967            // says the surface is a fixed point of printing.
968            (
969                "tests",
970                "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",
971            ),
972        ]
973    }
974
975    #[test]
976    fn printing_the_python_surface_round_trips() {
977        for (name, src) in corpus() {
978            let first = parse(name, src);
979            let printed = to_python(&first);
980            let leaked: &'static str = Box::leak(printed.clone().into_boxed_str());
981            let second = parse(name, leaked);
982            assert!(
983                first.structurally_eq(&second),
984                "{name} did not round-trip.\n--- printed ---\n{printed}\n--- as sexpr ---\n{}\n--- was ---\n{}",
985                to_sexpr(&second),
986                to_sexpr(&first)
987            );
988        }
989    }
990
991    #[test]
992    fn formatting_is_idempotent() {
993        for (name, src) in corpus() {
994            let once = to_python(&parse(name, src));
995            let leaked: &'static str = Box::leak(once.clone().into_boxed_str());
996            let twice = to_python(&parse(name, leaked));
997            assert_eq!(once, twice, "{name}: fmt is not idempotent");
998        }
999    }
1000}