Skip to main content

zerodds_lang_render/
python.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Python CST. Spec §5.
4//!
5//! One style, PEP 8: four spaces, two blank lines around a top-level
6//! class, one around a nested one, and a docstring as the first statement
7//! of every class body.
8//!
9//! ## Indentation is the grammar here
10//!
11//! In the other four renderers a wrong indent is untidy output. In Python
12//! it is a different program, or none. So the block structure is the only
13//! thing this file computes: [`Writer`] owns the depth, every body goes
14//! through `render_body`, and nothing else in this module writes a
15//! newline.
16//!
17//! ## The output is pure ASCII
18//!
19//! Every code point outside printable ASCII is escaped, in string
20//! literals and in docstrings alike. A generated file that is pure ASCII
21//! cannot be changed by an editor, a terminal or a CI runner that guessed
22//! the wrong encoding — and a wire test whose expected string arrived
23//! mangled would report a byte difference that is really a file-transfer
24//! difference.
25
26use crate::writer::{Writer, verbatim_lines};
27use alloc::format;
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
31use zerodds_lang_cst::python::{
32    Annotated, Arg, Assign, Class, Decorator, Expr, Function, Import, ImportName, Keyword, Module,
33    Param, Statement, Type,
34};
35
36/// Render a whole module.
37#[must_use]
38pub fn module(node: &Module) -> String {
39    let mut w = Writer::spaces4();
40    docstring(&mut w, &node.doc);
41    if !node.future.is_empty() {
42        w.blank_line();
43        let names: Vec<&str> = node.future.iter().map(Ident::as_str).collect();
44        w.line(&format!("from __future__ import {}", names.join(", ")));
45    }
46    if !node.imports.is_empty() {
47        w.blank_line();
48        for import in &node.imports {
49            w.line(&import_text(import));
50        }
51    }
52    render_body(&mut w, &node.body, true);
53    w.finish()
54}
55
56/// Render one statement, for a caller that has only one.
57#[must_use]
58pub fn statement(node: &Statement) -> String {
59    let mut w = Writer::spaces4();
60    render_statement(&mut w, node);
61    w.finish()
62}
63
64/// Render one annotation, for a caller that wants its spelling.
65#[must_use]
66pub fn type_(node: &Type) -> String {
67    type_text(node)
68}
69
70/// Render one expression, for a caller that wants its spelling.
71#[must_use]
72pub fn expr(node: &Expr) -> String {
73    expr_text(node)
74}
75
76fn import_text(import: &Import) -> String {
77    match import {
78        Import::Module { path, alias } => {
79            let mut out = format!("import {}", path_text(path));
80            if let Some(alias) = alias {
81                out.push_str(" as ");
82                out.push_str(alias.as_str());
83            }
84            out
85        }
86        Import::From { path, names } => {
87            let parts: Vec<String> = names.iter().map(import_name_text).collect();
88            format!("from {} import {}", path_text(path), parts.join(", "))
89        }
90    }
91}
92
93fn import_name_text(name: &ImportName) -> String {
94    match &name.alias {
95        Some(alias) => format!("{} as {}", name.name.as_str(), alias.as_str()),
96        None => name.name.as_str().to_string(),
97    }
98}
99
100/// A run of statements, with the blank lines PEP 8 puts between them.
101///
102/// `top` says whether this is the module body, which is the only thing
103/// that changes: two blank lines around a top-level definition and one
104/// around a nested one (PEP 8, "Blank Lines"). Bindings and comments run
105/// together, which is what a block of fields should look like.
106fn render_body(w: &mut Writer, body: &[Statement], top: bool) {
107    let gap = |w: &mut Writer| {
108        w.blank_line();
109        if top {
110            w.newline();
111        }
112    };
113    let mut previous_was_definition = false;
114    for (index, node) in body.iter().enumerate() {
115        let definition = matches!(node, Statement::Class(_) | Statement::Function(_));
116        if (index > 0 && (definition || previous_was_definition)) || (index == 0 && top) {
117            gap(w);
118        }
119        render_statement(w, node);
120        previous_was_definition = definition;
121    }
122}
123
124/// The body of a compound statement — an `if`, a loop, a `def`.
125///
126/// `pass` where the body is empty. Not the renderer deciding what the
127/// tree says: an empty suite is a `SyntaxError` and there is no program
128/// the tree could have meant. Every producer in this workspace fills the
129/// body itself, so this is the guard and not the mechanism —
130/// `crates/lang-rules/src/emit/python.rs` pads at each of the four
131/// constructors instead, where it can say *which* body was empty.
132fn render_suite(w: &mut Writer, body: &[Statement]) {
133    w.indent();
134    if body.is_empty() {
135        w.line("pass");
136    } else {
137        render_body(w, body, false);
138    }
139    w.dedent();
140}
141
142/// zerodds-lint: recursion-depth 64 (one level per nested block of a
143/// generated body. The codec walk emits a routine per type rather than
144/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
145/// its own framing and by one loop per array dimension, never by the
146/// type graph, whose descent stops at 64 anyway; the else-if chain, one
147/// arm per union case, is walked iteratively and costs no depth.)
148fn render_statement(w: &mut Writer, node: &Statement) {
149    match node {
150        Statement::Class(class) => render_class(w, class),
151        Statement::Function(function) => render_function(w, function),
152        Statement::Annotated(binding) => render_annotated(w, binding),
153        Statement::Assign(binding) => render_assign(w, binding),
154        Statement::Set { place, value } => {
155            w.line(&format!("{} = {}", expr_text(place), expr_text(value)));
156        }
157        Statement::Expr(value) => w.line(&expr_text(value)),
158        // The condition is written bare. `Expr::Binary` brackets itself,
159        // so `if (a == b):` is what a comparison renders as and the
160        // outer parentheses Python would allow are not added a second
161        // time.
162        Statement::If { .. } => render_if(w, node, "if"),
163        Statement::For {
164            counter,
165            bound,
166            body,
167        } => {
168            // `range(bound)` and not `range(0, bound)`: the walk's loop
169            // is always from zero, and the second argument would be a
170            // zero written into every generated loop for nothing.
171            w.line(&format!(
172                "for {} in range({}):",
173                counter.as_str(),
174                expr_text(bound)
175            ));
176            render_suite(w, body);
177        }
178        Statement::While { condition, body } => {
179            w.line(&format!("while {}:", expr_text(condition)));
180            render_suite(w, body);
181        }
182        Statement::Break => w.line("break"),
183        Statement::Return(value) => match value {
184            Some(value) => w.line(&format!("return {}", expr_text(value))),
185            None => w.line("return"),
186        },
187        Statement::Comment(doc) => comment(w, doc),
188        Statement::Pass => w.line("pass"),
189        Statement::Verbatim(text) => verbatim_lines(w, text),
190    }
191}
192
193/// An `if`, and its `else` written as `elif` where that is what it is.
194///
195/// `elif c:` *is* `else:` followed by an `if c:` — the reference calls
196/// the two equivalent (Python 3.13 reference §8.1) — so this changes no
197/// program. What it changes is the indentation, and that is not
198/// cosmetic here.
199///
200/// The shared walk builds a member chain as one `If` per member, each in
201/// the previous one's `else`. Rendered as nested suites, a `@mutable`
202/// aggregate of *n* members indents *n* levels deep, and CPython's
203/// tokenizer stops at 100 — `IndentationError: too many levels of
204/// indentation`, which is a `SyntaxError`, so the whole module fails to
205/// import rather than the one member failing to decode. Every other
206/// column's compiler has no such limit, which makes this the one place a
207/// chain six columns build identically stops being a program in exactly
208/// one of them.
209///
210/// **Not measured on the corpus**: its largest case declares twenty
211/// members and reaches twenty levels. The bound is CPython's, documented
212/// and fixed, and it is a property of any IDL a user writes rather than
213/// of the cases recorded here — which is the reason to render the chain
214/// flat now rather than after somebody's fifty-member type.
215///
216/// `keyword` is `"if"` at the head of a chain and `"elif"` inside it.
217///
218/// The next arm is taken in a loop and not by a call to this function
219/// again: the member count that would have indented CPython past its
220/// limit would equally have been the renderer's own stack depth.
221///
222/// zerodds-lint: recursion-depth 64 (suite nesting only; the arm chain
223/// is walked iteratively, and what remains is one level per nested suite
224/// of a generated body — the codec walk emits a routine per type rather
225/// than inlining, so a body nests by its framing and by one loop per
226/// array dimension, never by the type graph)
227fn render_if(w: &mut Writer, node: &Statement, keyword: &str) {
228    let mut node = node;
229    let mut keyword = keyword;
230    loop {
231        let Statement::If {
232            condition,
233            then,
234            otherwise,
235        } = node
236        else {
237            // Not reachable: the only caller matches on `Statement::If`
238            // first. Rendering the statement plainly is the answer that
239            // loses nothing if that ever stops being true.
240            render_statement(w, node);
241            return;
242        };
243        w.line(&format!("{keyword} {}:", expr_text(condition)));
244        render_suite(w, then);
245        let Some(otherwise) = otherwise else { return };
246        // One `If` and nothing else is a chain; anything else is a real
247        // `else` body. A trailing comment beside the `if` would make it
248        // the second, which is right — `elif` has nowhere to put one.
249        if let [nested @ Statement::If { .. }] = otherwise.as_slice() {
250            node = nested;
251            keyword = "elif";
252            continue;
253        }
254        w.line("else:");
255        render_suite(w, otherwise);
256        return;
257    }
258}
259
260fn render_function(w: &mut Writer, node: &Function) {
261    for decorator in &node.decorators {
262        w.line(&decorator_text(decorator));
263    }
264    let params: Vec<String> = node.params.iter().map(param_text).collect();
265    let mut head = format!("def {}({})", node.name.as_str(), params.join(", "));
266    if let Some(returns) = &node.returns {
267        head.push_str(" -> ");
268        head.push_str(&type_text(returns));
269    }
270    head.push(':');
271    w.line(&head);
272    w.indent();
273    docstring(w, &node.doc);
274    // Unlike a class, a `def` needs no blank line after its docstring:
275    // PEP 257 asks for one after a *class* docstring and says nothing
276    // for a function's, and a generated codec body reads as one run.
277    if node.body.is_empty() {
278        w.line("pass");
279    } else {
280        render_body(w, &node.body, false);
281    }
282    w.dedent();
283}
284
285fn param_text(node: &Param) -> String {
286    let mut out = node.name.as_str().to_string();
287    if let Some(type_) = &node.type_ {
288        out.push_str(": ");
289        out.push_str(&type_text(type_));
290    }
291    if let Some(default) = &node.default {
292        // ` = ` with spaces where the parameter is annotated and `=`
293        // without where it is not. PEP 8, "Function Annotations": the
294        // annotation is what changes the spelling, and a generator that
295        // wrote one form for both would be wrong for half its output.
296        out.push_str(if node.type_.is_some() { " = " } else { "=" });
297        out.push_str(&expr_text(default));
298    }
299    out
300}
301
302fn render_class(w: &mut Writer, node: &Class) {
303    for decorator in &node.decorators {
304        w.line(&decorator_text(decorator));
305    }
306    let mut head = format!("class {}", node.name.as_str());
307    if !node.bases.is_empty() {
308        let parts: Vec<String> = node.bases.iter().map(type_text).collect();
309        head.push('(');
310        head.push_str(&parts.join(", "));
311        head.push(')');
312    }
313    head.push(':');
314    w.line(&head);
315    w.indent();
316    docstring(w, &node.doc);
317    // The docstring counts as a statement, so a class carrying one and
318    // nothing else is already legal; a class carrying neither is not,
319    // and the tree says so with `Statement::Pass` rather than this
320    // renderer deciding to add one.
321    if !node.doc.is_empty() && !node.body.is_empty() {
322        w.blank_line();
323    }
324    // Inside a class the nesting is one blank line, never two, whatever
325    // the module body does.
326    render_body(w, &node.body, false);
327    w.dedent();
328}
329
330fn render_annotated(w: &mut Writer, node: &Annotated) {
331    comment(w, &node.doc);
332    let mut line = format!("{}: {}", node.name.as_str(), type_text(&node.type_));
333    if let Some(value) = &node.value {
334        line.push_str(" = ");
335        line.push_str(&expr_text(value));
336    }
337    w.line(&line);
338}
339
340fn render_assign(w: &mut Writer, node: &Assign) {
341    comment(w, &node.doc);
342    w.line(&format!(
343        "{} = {}",
344        node.name.as_str(),
345        expr_text(&node.value)
346    ));
347}
348
349fn decorator_text(node: &Decorator) -> String {
350    let mut out = format!("@{}", path_text(&node.path));
351    // A decorator with no arguments is written bare. `@dataclass` and
352    // `@dataclass()` are the same thing and the bare form is the
353    // conventional one.
354    if !node.args.is_empty() {
355        out.push('(');
356        let parts: Vec<String> = node.args.iter().map(arg_text).collect();
357        out.push_str(&parts.join(", "));
358        out.push(')');
359    }
360    out
361}
362
363fn arg_text(node: &Arg) -> String {
364    match node {
365        Arg::Positional(value) => expr_text(value),
366        Arg::Keyword(keyword) => keyword_text(keyword),
367    }
368}
369
370fn keyword_text(node: &Keyword) -> String {
371    format!("{}={}", node.name.as_str(), expr_text(&node.value))
372}
373
374/// A dotted name.
375///
376/// [`Path::is_rooted`] is not read: Python has no spelling for a rooted
377/// name — `a.b.C` is resolved against the enclosing scopes and there is
378/// no `::` to force the module scope — so a materialiser for this tree
379/// writes every reference from the module root and builds no rooted
380/// paths. A renderer that invented a marker would emit a name Python
381/// does not have.
382fn path_text(path: &Path) -> String {
383    let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
384    parts.join(".")
385}
386
387fn type_text(node: &Type) -> String {
388    match node {
389        Type::Name(path) => path_text(path),
390        Type::Subscript { base, args } => {
391            let parts: Vec<String> = args.iter().map(type_text).collect();
392            format!("{}[{}]", path_text(base), parts.join(", "))
393        }
394        Type::Optional(inner) => format!("typing.Optional[{}]", type_text(inner)),
395        Type::Annotated { base, markers } => {
396            let mut parts = alloc::vec![type_text(base)];
397            parts.extend(markers.iter().map(expr_text));
398            format!("typing.Annotated[{}]", parts.join(", "))
399        }
400    }
401}
402
403fn expr_text(node: &Expr) -> String {
404    match node {
405        Expr::Literal(literal) => literal_text(literal),
406        Expr::Path(path) => path_text(path),
407        Expr::Call { func, args } => {
408            let parts: Vec<String> = args.iter().map(arg_text).collect();
409            format!("{}({})", path_text(func), parts.join(", "))
410        }
411        // A one-element tuple needs the trailing comma, or the
412        // parentheses are grouping and the value is not a tuple at all.
413        Expr::Tuple(items) => {
414            let parts: Vec<String> = items.iter().map(expr_text).collect();
415            if parts.len() == 1 {
416                format!("({},)", parts[0])
417            } else {
418                format!("({})", parts.join(", "))
419            }
420        }
421        Expr::List(items) => {
422            let parts: Vec<String> = items.iter().map(expr_text).collect();
423            format!("[{}]", parts.join(", "))
424        }
425        Expr::Attribute { value, name } => {
426            format!("{}.{}", expr_text(value), name.as_str())
427        }
428        Expr::Index { value, index } => {
429            format!("{}[{}]", expr_text(value), expr_text(index))
430        }
431        Expr::Slice { start, end } => {
432            let side = |bound: &Option<alloc::boxed::Box<Expr>>| {
433                bound.as_deref().map_or_else(String::new, expr_text)
434            };
435            format!("{}:{}", side(start), side(end))
436        }
437        // The operator carries its own trailing space where one is
438        // needed — `not ` against `~` — because the tree is where that
439        // difference is recorded and not here.
440        Expr::Unary { op, value } => format!("{op}{}", expr_text(value)),
441        Expr::Paren(inner) => format!("({})", expr_text(inner)),
442        Expr::Dict(entries) => {
443            let parts: Vec<String> = entries
444                .iter()
445                .map(|(key, value)| format!("{}: {}", expr_text(key), expr_text(value)))
446                .collect();
447            format!("{{{}}}", parts.join(", "))
448        }
449        // Parenthesised: the tree records the nesting and precedence is
450        // then not something a reader has to reconstruct.
451        Expr::Binary { op, lhs, rhs } => {
452            format!("({} {op} {})", expr_text(lhs), expr_text(rhs))
453        }
454        Expr::Annotation(type_) => type_text(type_),
455    }
456}
457
458fn literal_text(literal: &Literal) -> String {
459    match literal {
460        Literal::Int(text) | Literal::Float(text) => text.clone(),
461        Literal::Bool(value) => {
462            if *value {
463                "True".to_string()
464            } else {
465                "False".to_string()
466            }
467        }
468        // Python has no character type: a character is a string of one.
469        Literal::Char(character) => {
470            let mut text = String::new();
471            text.push(*character);
472            string_literal(&text)
473        }
474        Literal::Str(text) => string_literal(text),
475        Literal::Bytes(bytes) => {
476            let mut out = String::from("b\"");
477            for byte in bytes {
478                out.push_str(&format!("\\x{byte:02x}"));
479            }
480            out.push('"');
481            out
482        }
483        Literal::Null => "None".to_string(),
484    }
485}
486
487/// A double-quoted Python string literal holding `text`, in ASCII.
488///
489/// Every code point outside printable ASCII becomes `\xXX`, `\uXXXX` or
490/// `\UXXXXXXXX`, which Python reads back as the code point it names.
491#[must_use]
492pub fn string_literal(text: &str) -> String {
493    let mut out = String::from("\"");
494    out.push_str(&escape_ascii(text));
495    out.push('"');
496    out
497}
498
499/// The body of a Python string literal: `text`, escaped down to ASCII.
500fn escape_ascii(text: &str) -> String {
501    let mut out = String::new();
502    for character in text.chars() {
503        match character {
504            '"' => out.push_str("\\\""),
505            '\\' => out.push_str("\\\\"),
506            '\n' => out.push_str("\\n"),
507            '\r' => out.push_str("\\r"),
508            '\t' => out.push_str("\\t"),
509            other => {
510                let point = u32::from(other);
511                if (0x20..0x7f).contains(&point) {
512                    out.push(other);
513                } else if point <= 0xff {
514                    out.push_str(&format!("\\x{point:02x}"));
515                } else if point <= 0xffff {
516                    out.push_str(&format!("\\u{point:04x}"));
517                } else {
518                    out.push_str(&format!("\\U{point:08x}"));
519                }
520            }
521        }
522    }
523    out
524}
525
526/// A docstring, as the first statement of a module or a class body.
527///
528/// Always the triple-quoted form and always closed on a line of its own
529/// when there is more than one line, which is PEP 257. The text is
530/// escaped exactly as a string literal is: a docstring is a string, and
531/// one carrying a backslash or a quote run would otherwise end early or
532/// mean something else.
533fn docstring(w: &mut Writer, doc: &Doc) {
534    if doc.is_empty() {
535        return;
536    }
537    let lines = doc.as_lines();
538    if lines.len() == 1 {
539        w.line(&format!("\"\"\"{}\"\"\"", escape_ascii(&lines[0])));
540        return;
541    }
542    w.line(&format!("\"\"\"{}", escape_ascii(&lines[0])));
543    for line in &lines[1..] {
544        if line.is_empty() {
545            w.newline();
546        } else {
547            w.line(&escape_ascii(line));
548        }
549    }
550    w.line("\"\"\"");
551}
552
553/// A `#` comment, one line per entry.
554fn comment(w: &mut Writer, doc: &Doc) {
555    for line in doc.as_lines() {
556        if line.is_empty() {
557            w.line("#");
558        } else {
559            w.line(&format!("# {}", escape_ascii(line)));
560        }
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
567    use super::*;
568    use alloc::boxed::Box;
569    use alloc::vec;
570
571    fn field(name: &str, type_: Type) -> Statement {
572        Statement::Annotated(Annotated {
573            doc: Doc::none(),
574            name: Ident::new(name),
575            type_,
576            value: None,
577        })
578    }
579
580    #[test]
581    fn a_class_body_is_indented_by_four_and_closed_by_the_dedent_alone() {
582        // Python has no closing delimiter, so the dedent *is* the end of
583        // the block; a renderer that lost it would emit a different
584        // program rather than untidy output.
585        let text = statement(&Statement::Class(Class {
586            doc: Doc::line("A point."),
587            decorators: Vec::new(),
588            name: Ident::new("Point"),
589            bases: Vec::new(),
590            body: vec![field("x", Type::name("int"))],
591        }));
592        assert_eq!(
593            text,
594            "class Point:\n    \"\"\"A point.\"\"\"\n\n    x: int\n"
595        );
596    }
597
598    #[test]
599    fn a_class_with_a_docstring_and_no_members_is_still_legal_python() {
600        let text = statement(&Statement::Class(Class {
601            doc: Doc::line("An IDL module."),
602            decorators: Vec::new(),
603            name: Ident::new("a"),
604            bases: Vec::new(),
605            body: Vec::new(),
606        }));
607        assert_eq!(text, "class a:\n    \"\"\"An IDL module.\"\"\"\n");
608    }
609
610    #[test]
611    fn an_empty_body_takes_the_pass_the_tree_carries() {
612        let text = statement(&Statement::Class(Class {
613            doc: Doc::none(),
614            decorators: Vec::new(),
615            name: Ident::new("E"),
616            bases: vec![Type::Name(Path::new(&["enum", "IntEnum"]))],
617            body: vec![Statement::Pass],
618        }));
619        assert_eq!(text, "class E(enum.IntEnum):\n    pass\n");
620    }
621
622    #[test]
623    fn an_import_carries_its_alias_and_its_from_form() {
624        assert_eq!(
625            import_text(&Import::Module {
626                path: Path::new(&["a", "b"]),
627                alias: Some(Ident::new("c")),
628            }),
629            "import a.b as c"
630        );
631        assert_eq!(
632            import_text(&Import::From {
633                path: Path::single("typing"),
634                names: vec![
635                    ImportName {
636                        name: Ident::new("Annotated"),
637                        alias: None,
638                    },
639                    ImportName {
640                        name: Ident::new("Optional"),
641                        alias: Some(Ident::new("Maybe")),
642                    },
643                ],
644            }),
645            "from typing import Annotated, Optional as Maybe"
646        );
647    }
648
649    #[test]
650    fn a_call_takes_positional_and_keyword_arguments_in_written_order() {
651        assert_eq!(
652            expr(&Expr::Call {
653                func: Path::single("f"),
654                args: vec![
655                    Arg::Positional(Expr::int(1)),
656                    Arg::Keyword(Keyword {
657                        name: Ident::new("b"),
658                        value: Expr::Literal(Literal::Bool(false)),
659                    }),
660                ],
661            }),
662            "f(1, b=False)"
663        );
664    }
665
666    #[test]
667    fn a_decorator_with_no_arguments_is_written_bare() {
668        let node = Decorator {
669            path: Path::new(&["dataclasses", "dataclass"]),
670            args: Vec::new(),
671        };
672        assert_eq!(decorator_text(&node), "@dataclasses.dataclass");
673        let node = Decorator {
674            path: Path::new(&["dataclasses", "dataclass"]),
675            args: vec![Arg::Keyword(Keyword {
676                name: Ident::new("kw_only"),
677                value: Expr::Literal(Literal::Bool(true)),
678            })],
679        };
680        assert_eq!(
681            decorator_text(&node),
682            "@dataclasses.dataclass(kw_only=True)"
683        );
684    }
685
686    #[test]
687    fn an_annotated_type_keeps_its_metadata_in_the_annotation() {
688        let node = Type::Annotated {
689            base: Box::new(Type::name("int")),
690            markers: vec![Expr::Call {
691                func: Path::single("IdlInt"),
692                args: vec![
693                    Arg::Keyword(Keyword {
694                        name: Ident::new("bits"),
695                        value: Expr::int(32),
696                    }),
697                    Arg::Keyword(Keyword {
698                        name: Ident::new("signed"),
699                        value: Expr::Literal(Literal::Bool(true)),
700                    }),
701                ],
702            }],
703        };
704        assert_eq!(
705            type_(&node),
706            "typing.Annotated[int, IdlInt(bits=32, signed=True)]"
707        );
708    }
709
710    #[test]
711    fn an_exact_length_tuple_and_a_list_are_spelled_apart() {
712        // The difference a reader has to see: a `list` can be appended
713        // to and a `tuple[int, int, int]` cannot even be the wrong
714        // length.
715        assert_eq!(
716            type_(&Type::subscript(
717                Path::single("list"),
718                vec![Type::name("int")]
719            )),
720            "list[int]"
721        );
722        assert_eq!(
723            type_(&Type::subscript(
724                Path::single("tuple"),
725                vec![Type::name("int"), Type::name("int")]
726            )),
727            "tuple[int, int]"
728        );
729    }
730
731    #[test]
732    fn a_one_element_tuple_keeps_its_trailing_comma() {
733        // Without it the parentheses are grouping and the value is not a
734        // tuple at all.
735        assert_eq!(expr(&Expr::Tuple(vec![Expr::int(1)])), "(1,)");
736        assert_eq!(
737            expr(&Expr::Tuple(vec![Expr::int(1), Expr::int(2)])),
738            "(1, 2)"
739        );
740    }
741
742    #[test]
743    fn every_literal_is_written_in_the_form_python_reads_back() {
744        assert_eq!(literal_text(&Literal::Bool(true)), "True");
745        assert_eq!(literal_text(&Literal::Null), "None");
746        assert_eq!(literal_text(&Literal::Char('a')), "\"a\"");
747        assert_eq!(literal_text(&Literal::Int("-1".to_string())), "-1");
748        assert_eq!(
749            literal_text(&Literal::Bytes(vec![0x00, 0xff])),
750            "b\"\\x00\\xff\""
751        );
752    }
753
754    #[test]
755    fn a_string_literal_leaves_the_file_pure_ascii() {
756        assert_eq!(string_literal("Z"), "\"Z\"");
757        // One byte's worth takes `\xNN`; Python reads it as the code
758        // point and not as a byte, which is why a `str` literal may use
759        // it at all.
760        assert_eq!(string_literal("\u{e4}"), "\"\\xe4\"");
761        assert_eq!(string_literal("\u{2603}"), "\"\\u2603\"");
762        // Outside the Basic Multilingual Plane, which is the case the
763        // wstring length rule turns on.
764        assert_eq!(string_literal("\u{1d11e}"), "\"\\U0001d11e\"");
765        assert_eq!(string_literal("a\"b\\c"), "\"a\\\"b\\\\c\"");
766    }
767
768    #[test]
769    fn a_docstring_escapes_what_would_close_it_early() {
770        let mut w = Writer::spaces4();
771        docstring(&mut w, &Doc::line("a \"quoted\" \\ backslash"));
772        assert_eq!(w.finish(), "\"\"\"a \\\"quoted\\\" \\\\ backslash\"\"\"\n");
773    }
774
775    #[test]
776    fn a_multi_line_docstring_closes_on_a_line_of_its_own() {
777        let mut w = Writer::spaces4();
778        docstring(&mut w, &Doc::lines(&["first", "", "third"]));
779        assert_eq!(w.finish(), "\"\"\"first\n\nthird\n\"\"\"\n");
780    }
781
782    #[test]
783    fn a_module_puts_the_future_import_before_everything_but_the_docstring() {
784        // A `from __future__` line that is not the first statement is a
785        // SyntaxError, not a style problem.
786        let text = module(&Module {
787            doc: Doc::line("Generated."),
788            future: vec![Ident::new("annotations")],
789            imports: vec![Import::Module {
790                path: Path::single("dataclasses"),
791                alias: None,
792            }],
793            body: vec![field("X", Type::name("int"))],
794        });
795        assert_eq!(
796            text,
797            "\"\"\"Generated.\"\"\"\n\nfrom __future__ import annotations\n\nimport dataclasses\n\n\nX: int\n"
798        );
799    }
800
801    /// The four compound statements the codec walk builds, each with its
802    /// suite indented under it and closed by the dedent alone.
803    #[test]
804    fn a_compound_statement_indents_its_suite_and_is_closed_by_the_dedent() {
805        let effect = |text: &str| Statement::Expr(Expr::name(text));
806        assert_eq!(
807            statement(&Statement::If {
808                condition: Expr::Binary {
809                    op: "==",
810                    lhs: Box::new(Expr::name("a")),
811                    rhs: Box::new(Expr::int(1)),
812                },
813                then: vec![effect("f()")],
814                otherwise: Some(vec![effect("g()")]),
815            }),
816            "if (a == 1):\n    f()\nelse:\n    g()\n"
817        );
818        // An `else` holding one `if` and nothing else is a chain, and
819        // `elif` is what that tree is. Rendered as nested suites, one
820        // `If` per member of a `@mutable` aggregate indents one level
821        // per member and CPython stops at 100.
822        assert_eq!(
823            statement(&Statement::If {
824                condition: Expr::name("a"),
825                then: vec![effect("f()")],
826                otherwise: Some(vec![Statement::If {
827                    condition: Expr::name("b"),
828                    then: vec![effect("g()")],
829                    otherwise: Some(vec![effect("h()")]),
830                }]),
831            }),
832            "if a:\n    f()\nelif b:\n    g()\nelse:\n    h()\n"
833        );
834        // Two statements in the `else` is a real `else` body and not a
835        // chain, because `elif` has nowhere to put the second.
836        assert_eq!(
837            statement(&Statement::If {
838                condition: Expr::name("a"),
839                then: vec![effect("f()")],
840                otherwise: Some(vec![
841                    Statement::If {
842                        condition: Expr::name("b"),
843                        then: vec![effect("g()")],
844                        otherwise: None,
845                    },
846                    effect("h()"),
847                ]),
848            }),
849            "if a:\n    f()\nelse:\n    if b:\n        g()\n    h()\n"
850        );
851        assert_eq!(
852            statement(&Statement::For {
853                counter: Ident::new("i"),
854                bound: Expr::name("n"),
855                body: vec![effect("f()")],
856            }),
857            "for i in range(n):\n    f()\n"
858        );
859        assert_eq!(
860            statement(&Statement::While {
861                condition: Expr::name("more"),
862                body: vec![Statement::Break],
863            }),
864            "while more:\n    break\n"
865        );
866        assert_eq!(
867            statement(&Statement::Return(Some(Expr::name("v")))),
868            "return v\n"
869        );
870        assert_eq!(statement(&Statement::Return(None)), "return\n");
871    }
872
873    /// An empty suite is a `SyntaxError`, not untidy output.
874    #[test]
875    fn an_empty_suite_takes_a_pass_rather_than_rendering_to_nothing() {
876        assert_eq!(
877            statement(&Statement::If {
878                condition: Expr::name("a"),
879                then: Vec::new(),
880                otherwise: None,
881            }),
882            "if a:\n    pass\n"
883        );
884        assert_eq!(
885            statement(&Statement::Function(Function {
886                doc: Doc::none(),
887                decorators: Vec::new(),
888                name: Ident::new("f"),
889                params: Vec::new(),
890                returns: None,
891                body: Vec::new(),
892            })),
893            "def f():\n    pass\n"
894        );
895    }
896
897    #[test]
898    fn a_function_writes_its_annotations_and_its_return_type() {
899        let text = statement(&Statement::Function(Function {
900            doc: Doc::line("Write one sample."),
901            decorators: Vec::new(),
902            name: Ident::new("zd_enc_Point"),
903            params: vec![
904                Param {
905                    name: Ident::new("w"),
906                    type_: Some(Type::Name(Path::new(&["zerodds_wire", "Writer"]))),
907                    default: None,
908                },
909                Param {
910                    name: Ident::new("v"),
911                    type_: Some(Type::Name(Path::new(&["a", "Point"]))),
912                    default: None,
913                },
914            ],
915            returns: Some(Type::name("None")),
916            body: vec![Statement::Expr(Expr::Call {
917                func: Path::new(&["w", "write_i32"]),
918                args: vec![Arg::Positional(Expr::Attribute {
919                    value: Box::new(Expr::name("v")),
920                    name: Ident::new("x"),
921                })],
922            })],
923        }));
924        assert_eq!(
925            text,
926            "def zd_enc_Point(w: zerodds_wire.Writer, v: a.Point) -> None:\n    \
927             \"\"\"Write one sample.\"\"\"\n    w.write_i32(v.x)\n"
928        );
929    }
930
931    /// PEP 8, "Function Annotations": an annotated parameter's default
932    /// takes spaces around the `=` and an unannotated one does not.
933    #[test]
934    fn a_default_is_spaced_only_where_the_parameter_is_annotated() {
935        assert_eq!(
936            param_text(&Param {
937                name: Ident::new("order"),
938                type_: Some(Type::name("str")),
939                default: Some(Expr::Literal(Literal::Str("stream".to_string()))),
940            }),
941            "order: str = \"stream\""
942        );
943        assert_eq!(
944            param_text(&Param {
945                name: Ident::new("order"),
946                type_: None,
947                default: Some(Expr::int(2)),
948            }),
949            "order=2"
950        );
951    }
952
953    /// A store writes into a place; a binding introduces a name. Both
954    /// render `x = y` and the tree keeps them apart.
955    #[test]
956    fn a_store_reaches_a_place_a_binding_could_not_name() {
957        assert_eq!(
958            statement(&Statement::Set {
959                place: Expr::Index {
960                    value: Box::new(Expr::name("v")),
961                    index: Box::new(Expr::name("i")),
962                },
963                value: Expr::int(0),
964            }),
965            "v[i] = 0\n"
966        );
967        assert_eq!(
968            statement(&Statement::Set {
969                place: Expr::Attribute {
970                    value: Box::new(Expr::name("v")),
971                    name: Ident::new("field"),
972                },
973                value: Expr::Literal(Literal::Null),
974            }),
975            "v.field = None\n"
976        );
977    }
978
979    /// An attribute of a computed value, which `Expr::Path` cannot hold
980    /// because its object is not a name.
981    #[test]
982    fn an_attribute_of_a_subscript_is_not_a_dotted_name() {
983        assert_eq!(
984            expr(&Expr::Attribute {
985                value: Box::new(Expr::Index {
986                    value: Box::new(Expr::name("v")),
987                    index: Box::new(Expr::int(0)),
988                }),
989                name: Ident::new("key"),
990            }),
991            "v[0].key"
992        );
993    }
994
995    /// The operator carries its own trailing space: `notx` is an
996    /// identifier and `~x` takes none.
997    #[test]
998    fn a_unary_operator_carries_the_space_python_needs_and_no_other() {
999        assert_eq!(
1000            expr(&Expr::Unary {
1001                op: "not ",
1002                value: Box::new(Expr::name("ok")),
1003            }),
1004            "not ok"
1005        );
1006        assert_eq!(
1007            expr(&Expr::Unary {
1008                op: "~",
1009                value: Box::new(Expr::name("bits")),
1010            }),
1011            "~bits"
1012        );
1013        assert_eq!(expr(&Expr::Paren(Box::new(Expr::name("a")))), "(a)");
1014    }
1015
1016    /// §7.3.1.2.1.12.3's TRIM keeps a prefix, so only the far end is
1017    /// ever written and the near one renders as nothing.
1018    #[test]
1019    fn a_slice_writes_nothing_for_an_absent_end() {
1020        assert_eq!(
1021            expr(&Expr::Index {
1022                value: Box::new(Expr::name("v")),
1023                index: Box::new(Expr::Slice {
1024                    start: None,
1025                    end: Some(Box::new(Expr::int(5))),
1026                }),
1027            }),
1028            "v[:5]"
1029        );
1030        assert_eq!(
1031            expr(&Expr::Slice {
1032                start: None,
1033                end: None
1034            }),
1035            ":"
1036        );
1037    }
1038
1039    /// A list is written into and a tuple is not, which is the whole
1040    /// reason the reader's collection slots are one and not the other.
1041    #[test]
1042    fn a_list_is_spelled_apart_from_a_tuple() {
1043        assert_eq!(
1044            expr(&Expr::List(vec![Expr::Literal(Literal::Null)])),
1045            "[None]"
1046        );
1047        assert_eq!(expr(&Expr::List(Vec::new())), "[]");
1048        assert_eq!(expr(&Expr::Tuple(vec![Expr::int(1)])), "(1,)");
1049    }
1050
1051    #[test]
1052    fn a_blank_line_never_carries_indentation() {
1053        // Trailing whitespace on a blank line inside a class body is
1054        // what a formatter strips and what a byte comparison then
1055        // reports as a difference.
1056        let text = statement(&Statement::Class(Class {
1057            doc: Doc::line("Two fields."),
1058            decorators: Vec::new(),
1059            name: Ident::new("P"),
1060            bases: Vec::new(),
1061            body: vec![field("a", Type::name("int")), field("b", Type::name("int"))],
1062        }));
1063        for line in text.lines() {
1064            assert_eq!(line, line.trim_end(), "trailing whitespace: {line:?}");
1065        }
1066    }
1067}