Skip to main content

zerodds_lang_render/
rust.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Rust CST. Spec §5.
4//!
5//! One style, the one `rustfmt` produces: four spaces, braces on the
6//! same line, one item per line with a blank line between items.
7
8use crate::writer::{Writer, comment_lines, escape_char, escape_string, verbatim_lines};
9use alloc::format;
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12use zerodds_lang_cst::common::{Doc, Ident, Literal, Path, Visibility};
13use zerodds_lang_cst::rust::{
14    Attr, AttrArg, Block, Const, Derive, Enum, Expr, Field, File, Function, GenericParam, Impl,
15    Item, MatchArm, Module, Param, Pattern, Receiver, Statement, Struct, StructBody, Trait, Type,
16    TypeAlias, Use, Variant, VariantBody,
17};
18
19/// Render a whole file.
20#[must_use]
21pub fn file(file: &File) -> String {
22    let mut w = Writer::spaces4();
23    for attr in &file.inner_attrs {
24        w.line(&attr_text(attr, true));
25    }
26    if !file.inner_attrs.is_empty() {
27        w.blank_line();
28    }
29    doc(&mut w, &file.doc);
30    for use_ in &file.uses {
31        w.line(&use_text(use_));
32    }
33    if !file.uses.is_empty() {
34        w.blank_line();
35    }
36    for (index, item) in file.items.iter().enumerate() {
37        if index > 0 {
38            w.blank_line();
39        }
40        render_item(&mut w, item);
41    }
42    w.finish()
43}
44
45/// Render one item, for a caller that has only one.
46#[must_use]
47pub fn item(node: &Item) -> String {
48    let mut w = Writer::spaces4();
49    render_item(&mut w, node);
50    w.finish()
51}
52
53/// Render one type, for a caller that has only one.
54#[must_use]
55pub fn type_(node: &Type) -> String {
56    type_text(node)
57}
58
59/// Render one expression, for a caller that has only one.
60#[must_use]
61pub fn expr(node: &Expr) -> String {
62    expr_text(node)
63}
64
65/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
66/// module chain is cut at `MAX_MODULE_DEPTH` in
67/// `lang-rules/src/adapter/cst.rs`)
68fn render_item(w: &mut Writer, node: &Item) {
69    match node {
70        Item::Module(module) => render_module(w, module),
71        Item::Struct(s) => render_struct(w, s),
72        Item::Enum(e) => render_enum(w, e),
73        Item::TypeAlias(alias) => render_alias(w, alias),
74        Item::Const(constant) => render_const(w, constant),
75        Item::Trait(t) => render_trait(w, t),
76        Item::Impl(i) => render_impl(w, i),
77        Item::Function(f) => render_function(w, f),
78        Item::Verbatim(text) => verbatim_lines(w, text),
79    }
80}
81
82/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
83/// module chain is cut at `MAX_MODULE_DEPTH` in
84/// `lang-rules/src/adapter/cst.rs`)
85fn render_module(w: &mut Writer, node: &Module) {
86    doc(w, &node.doc);
87    attrs(w, &node.attrs);
88    let mut head = String::new();
89    head.push_str(&vis_text(node.visibility));
90    head.push_str("mod ");
91    head.push_str(node.name.as_str());
92    head.push_str(" {");
93    w.line(&head);
94    w.indent();
95    for use_ in &node.uses {
96        w.line(&use_text(use_));
97    }
98    if !node.uses.is_empty() && !node.items.is_empty() {
99        w.blank_line();
100    }
101    for (index, item) in node.items.iter().enumerate() {
102        if index > 0 {
103            w.blank_line();
104        }
105        render_item(w, item);
106    }
107    w.dedent();
108    w.line("}");
109}
110
111fn render_struct(w: &mut Writer, node: &Struct) {
112    doc(w, &node.doc);
113    attrs(w, &node.attrs);
114    derives(w, &node.derives);
115    let mut head = String::new();
116    head.push_str(&vis_text(node.visibility));
117    head.push_str("struct ");
118    head.push_str(node.name.as_str());
119    head.push_str(&generics_text(&node.generics));
120
121    let text = &node.body_text;
122    let has_text = !text.begin.is_empty() || !text.end.is_empty();
123    match &node.body {
124        StructBody::Unit if has_text => {
125            head.push_str(" {");
126            w.line(&head);
127            w.indent();
128            for line in text.begin.iter().chain(&text.end) {
129                verbatim_lines(w, line);
130            }
131            w.dedent();
132            w.line("}");
133        }
134        StructBody::Unit => {
135            head.push(';');
136            w.line(&head);
137        }
138        StructBody::Tuple(types) => {
139            head.push('(');
140            let parts: Vec<String> = types.iter().map(type_text).collect();
141            head.push_str(&parts.join(", "));
142            head.push_str(");");
143            w.line(&head);
144        }
145        StructBody::Named(fields) => {
146            head.push_str(" {");
147            w.line(&head);
148            w.indent();
149            for line in &text.begin {
150                verbatim_lines(w, line);
151            }
152            for field in fields {
153                render_field(w, field);
154            }
155            for line in &text.end {
156                verbatim_lines(w, line);
157            }
158            w.dedent();
159            w.line("}");
160        }
161    }
162}
163
164fn render_field(w: &mut Writer, node: &Field) {
165    doc(w, &node.doc);
166    attrs(w, &node.attrs);
167    let mut line = String::new();
168    line.push_str(&vis_text(node.visibility));
169    line.push_str(node.name.as_str());
170    line.push_str(": ");
171    line.push_str(&type_text(&node.type_));
172    line.push(',');
173    w.line(&line);
174}
175
176fn render_enum(w: &mut Writer, node: &Enum) {
177    doc(w, &node.doc);
178    attrs(w, &node.attrs);
179    derives(w, &node.derives);
180    let mut head = String::new();
181    head.push_str(&vis_text(node.visibility));
182    head.push_str("enum ");
183    head.push_str(node.name.as_str());
184    head.push_str(&generics_text(&node.generics));
185    head.push_str(" {");
186    w.line(&head);
187    w.indent();
188    for variant in &node.variants {
189        render_variant(w, variant);
190    }
191    w.dedent();
192    w.line("}");
193}
194
195fn render_variant(w: &mut Writer, node: &Variant) {
196    doc(w, &node.doc);
197    attrs(w, &node.attrs);
198    let mut line = String::from(node.name.as_str());
199    match &node.body {
200        VariantBody::Unit => {}
201        VariantBody::Tuple(types) => {
202            line.push('(');
203            let parts: Vec<String> = types.iter().map(type_text).collect();
204            line.push_str(&parts.join(", "));
205            line.push(')');
206        }
207        VariantBody::Named(fields) => {
208            line.push_str(" {");
209            w.line(&line);
210            w.indent();
211            for field in fields {
212                render_field(w, field);
213            }
214            w.dedent();
215            w.line("},");
216            return;
217        }
218    }
219    if let Some(value) = &node.discriminant {
220        line.push_str(" = ");
221        line.push_str(&expr_text(value));
222    }
223    line.push(',');
224    w.line(&line);
225}
226
227fn render_alias(w: &mut Writer, node: &TypeAlias) {
228    doc(w, &node.doc);
229    attrs(w, &node.attrs);
230    let mut line = String::new();
231    line.push_str(&vis_text(node.visibility));
232    line.push_str("type ");
233    line.push_str(node.name.as_str());
234    line.push_str(&generics_text(&node.generics));
235    line.push_str(" = ");
236    line.push_str(&type_text(&node.type_));
237    line.push(';');
238    w.line(&line);
239}
240
241fn render_const(w: &mut Writer, node: &Const) {
242    doc(w, &node.doc);
243    attrs(w, &node.attrs);
244    let mut line = String::new();
245    line.push_str(&vis_text(node.visibility));
246    line.push_str("const ");
247    line.push_str(node.name.as_str());
248    line.push_str(": ");
249    line.push_str(&type_text(&node.type_));
250    line.push_str(" = ");
251    line.push_str(&expr_text(&node.value));
252    line.push(';');
253    w.line(&line);
254}
255
256/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
257/// module chain is cut at `MAX_MODULE_DEPTH` in
258/// `lang-rules/src/adapter/cst.rs`)
259fn render_trait(w: &mut Writer, node: &Trait) {
260    doc(w, &node.doc);
261    attrs(w, &node.attrs);
262    let mut head = String::new();
263    head.push_str(&vis_text(node.visibility));
264    head.push_str("trait ");
265    head.push_str(node.name.as_str());
266    head.push_str(&generics_text(&node.generics));
267    if !node.supertraits.is_empty() {
268        head.push_str(": ");
269        let parts: Vec<String> = node.supertraits.iter().map(type_text).collect();
270        head.push_str(&parts.join(" + "));
271    }
272    head.push_str(" {");
273    w.line(&head);
274    w.indent();
275    for (index, item) in node.items.iter().enumerate() {
276        if index > 0 {
277            w.blank_line();
278        }
279        render_item(w, item);
280    }
281    w.dedent();
282    w.line("}");
283}
284
285/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
286/// module chain is cut at `MAX_MODULE_DEPTH` in
287/// `lang-rules/src/adapter/cst.rs`)
288fn render_impl(w: &mut Writer, node: &Impl) {
289    doc(w, &node.doc);
290    attrs(w, &node.attrs);
291    let mut head = String::from("impl");
292    head.push_str(&generics_text(&node.generics));
293    head.push(' ');
294    if let Some(trait_) = &node.trait_ {
295        head.push_str(&type_text(trait_));
296        head.push_str(" for ");
297    }
298    head.push_str(&type_text(&node.self_type));
299    head.push_str(" {");
300    w.line(&head);
301    w.indent();
302    for (index, item) in node.items.iter().enumerate() {
303        if index > 0 {
304            w.blank_line();
305        }
306        render_item(w, item);
307    }
308    w.dedent();
309    w.line("}");
310}
311
312fn render_function(w: &mut Writer, node: &Function) {
313    doc(w, &node.doc);
314    attrs(w, &node.attrs);
315    let mut head = String::new();
316    head.push_str(&vis_text(node.visibility));
317    head.push_str("fn ");
318    head.push_str(node.name.as_str());
319    head.push_str(&generics_text(&node.generics));
320    head.push('(');
321    let parts: Vec<String> = node.params.iter().map(param_text).collect();
322    head.push_str(&parts.join(", "));
323    head.push(')');
324    if let Some(returns) = &node.returns {
325        head.push_str(" -> ");
326        head.push_str(&type_text(returns));
327    }
328    match &node.body {
329        None => {
330            head.push(';');
331            w.line(&head);
332        }
333        Some(block) => {
334            head.push_str(" {");
335            w.line(&head);
336            w.indent();
337            render_block(w, block);
338            w.dedent();
339            w.line("}");
340        }
341    }
342}
343
344/// The `if` an else-block consists of, when it consists of nothing else.
345fn sole_if(block: &Block) -> Option<&Expr> {
346    match block.statements.as_slice() {
347        [Statement::Semi(inner @ Expr::If { .. }) | Statement::Tail(inner @ Expr::If { .. })] => {
348            Some(inner)
349        }
350        _ => None,
351    }
352}
353
354/// A closure's body. A block of one-line statements stays on the
355/// closure's line — `|w, v| { w.write_i32(*v); Ok(()) }` — because the
356/// closure is an argument inside a longer line, and a block rendered over
357/// several lines there would carry the indentation of nothing around it.
358///
359/// zerodds-lint: recursion-depth = the built expression's nesting, as for
360/// `expr_text`, which this is one arm of.
361fn closure_body_text(body: &Expr) -> String {
362    let Expr::Block(block) = body else {
363        return expr_text(body);
364    };
365    let mut parts = Vec::with_capacity(block.statements.len());
366    for statement in &block.statements {
367        let text = match statement {
368            Statement::Semi(value) => format!("{};", expr_text(value)),
369            Statement::Tail(value) => expr_text(value),
370            Statement::Let { .. } | Statement::Item(_) => return expr_text(body),
371        };
372        if text.contains('\n') {
373            return expr_text(body);
374        }
375        parts.push(text);
376    }
377    format!("{{ {} }}", parts.join(" "))
378}
379
380fn render_block(w: &mut Writer, block: &Block) {
381    for statement in &block.statements {
382        render_statement(w, statement);
383    }
384}
385
386fn render_statement(w: &mut Writer, node: &Statement) {
387    match node {
388        Statement::Let {
389            name,
390            type_,
391            value,
392            mutable,
393        } => {
394            let mut line = String::from("let ");
395            if *mutable {
396                line.push_str("mut ");
397            }
398            line.push_str(name.as_str());
399            if let Some(type_) = type_ {
400                line.push_str(": ");
401                line.push_str(&type_text(type_));
402            }
403            if let Some(value) = value {
404                line.push_str(" = ");
405                line.push_str(&expr_text(value));
406            }
407            line.push(';');
408            w.line(&line);
409        }
410        Statement::Semi(value) => {
411            let mut line = expr_text(value);
412            line.push(';');
413            w.line(&line);
414        }
415        Statement::Tail(value) => w.line(&expr_text(value)),
416        Statement::Item(item) => render_item(w, item),
417    }
418}
419
420fn param_text(node: &Param) -> String {
421    if let Some(receiver) = node.receiver {
422        return match receiver {
423            Receiver::Value => "self".to_string(),
424            Receiver::Ref => "&self".to_string(),
425            Receiver::RefMut => "&mut self".to_string(),
426        };
427    }
428    let mut out = String::new();
429    if let Some(name) = &node.name {
430        out.push_str(name.as_str());
431        out.push_str(": ");
432    }
433    out.push_str(&type_text(&node.type_));
434    out
435}
436
437fn generics_text(params: &[GenericParam]) -> String {
438    if params.is_empty() {
439        return String::new();
440    }
441    let mut out = String::from("<");
442    let parts: Vec<String> = params
443        .iter()
444        .map(|param| {
445            let mut text = String::new();
446            if param.lifetime {
447                text.push('\'');
448            }
449            text.push_str(param.name.as_str());
450            if !param.bounds.is_empty() {
451                text.push_str(": ");
452                let bounds: Vec<String> = param.bounds.iter().map(type_text).collect();
453                text.push_str(&bounds.join(" + "));
454            }
455            text
456        })
457        .collect();
458    out.push_str(&parts.join(", "));
459    out.push('>');
460    out
461}
462
463fn vis_text(visibility: Visibility) -> String {
464    match visibility {
465        Visibility::Public => "pub ".to_string(),
466        Visibility::Internal => "pub(crate) ".to_string(),
467        Visibility::Private => String::new(),
468    }
469}
470
471fn doc(w: &mut Writer, doc: &Doc) {
472    if doc.is_empty() {
473        return;
474    }
475    comment_lines(w, "/// ", doc.as_lines());
476}
477
478fn attrs(w: &mut Writer, attrs: &[Attr]) {
479    for attr in attrs {
480        w.line(&attr_text(attr, false));
481    }
482}
483
484/// Derives render as one `#[derive(..)]` listing them in order.
485fn derives(w: &mut Writer, derives: &[Derive]) {
486    if derives.is_empty() {
487        return;
488    }
489    let names: Vec<String> = derives.iter().map(|d| path_text(&d.path)).collect();
490    let mut line = String::from("#[derive(");
491    line.push_str(&names.join(", "));
492    line.push_str(")]");
493    w.line(&line);
494}
495
496fn attr_text(attr: &Attr, inner: bool) -> String {
497    let mut out = String::from(if inner { "#![" } else { "#[" });
498    out.push_str(&path_text(&attr.path));
499    if !attr.args.is_empty() {
500        out.push('(');
501        let parts: Vec<String> = attr.args.iter().map(attr_arg_text).collect();
502        out.push_str(&parts.join(", "));
503        out.push(')');
504    }
505    out.push(']');
506    out
507}
508
509fn attr_arg_text(arg: &AttrArg) -> String {
510    match arg {
511        AttrArg::Word(name) => name.as_str().to_string(),
512        AttrArg::Pair { name, value } => {
513            let mut out = name.as_str().to_string();
514            out.push_str(" = ");
515            out.push_str(&literal_text(value));
516            out
517        }
518        AttrArg::Nested { name, args } => {
519            let mut out = name.as_str().to_string();
520            out.push('(');
521            let parts: Vec<String> = args.iter().map(attr_arg_text).collect();
522            out.push_str(&parts.join(", "));
523            out.push(')');
524            out
525        }
526    }
527}
528
529fn use_text(node: &Use) -> String {
530    let mut out = vis_text(node.visibility);
531    out.push_str("use ");
532    out.push_str(&path_text(&node.path));
533    match node.names.len() {
534        0 => {}
535        1 => {
536            out.push_str("::");
537            out.push_str(node.names[0].as_str());
538        }
539        _ => {
540            out.push_str("::{");
541            let parts: Vec<&str> = node.names.iter().map(Ident::as_str).collect();
542            out.push_str(&parts.join(", "));
543            out.push('}');
544        }
545    }
546    out.push(';');
547    out
548}
549
550fn path_text(path: &Path) -> String {
551    let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
552    let joined = parts.join("::");
553    if path.is_rooted() {
554        let mut out = String::from("crate::");
555        out.push_str(&joined);
556        return out;
557    }
558    joined
559}
560
561/// zerodds-lint: recursion-depth = the spelled type's nesting, one level
562/// per collection or alias. `rust_type_of` in
563/// `lang-rules/src/adapter/cst.rs` carries no depth guard of its own, so
564/// the bound is the frontend's — `MAX_ALIAS_DEPTH` in
565/// `idl-lower/src/validate.rs` for an alias chain — and, for collections,
566/// the nesting the IDL source spells. No number is claimed: this
567/// signature returns no error.
568fn type_text(node: &Type) -> String {
569    match node {
570        Type::Named { path, args } => {
571            let mut out = path_text(path);
572            if !args.is_empty() {
573                out.push('<');
574                let parts: Vec<String> = args.iter().map(type_text).collect();
575                out.push_str(&parts.join(", "));
576                out.push('>');
577            }
578            out
579        }
580        Type::Ref {
581            target,
582            mutable,
583            lifetime,
584        } => {
585            let mut out = String::from("&");
586            if let Some(lifetime) = lifetime {
587                out.push('\'');
588                out.push_str(lifetime.as_str());
589                out.push(' ');
590            }
591            if *mutable {
592                out.push_str("mut ");
593            }
594            out.push_str(&type_text(target));
595            out
596        }
597        Type::Array { element, length } => {
598            let mut out = String::from("[");
599            out.push_str(&type_text(element));
600            out.push_str("; ");
601            out.push_str(&expr_text(length));
602            out.push(']');
603            out
604        }
605        Type::Slice(element) => {
606            let mut out = String::from("[");
607            out.push_str(&type_text(element));
608            out.push(']');
609            out
610        }
611        Type::Tuple(elements) => {
612            let mut out = String::from("(");
613            let parts: Vec<String> = elements.iter().map(type_text).collect();
614            out.push_str(&parts.join(", "));
615            // A one-element tuple needs the comma or it is a parenthesis.
616            if elements.len() == 1 {
617                out.push(',');
618            }
619            out.push(')');
620            out
621        }
622        Type::Primitive(primitive) => primitive.as_str().to_string(),
623        Type::ImplTrait(bounds) => {
624            let mut out = String::from("impl ");
625            let parts: Vec<String> = bounds.iter().map(type_text).collect();
626            out.push_str(&parts.join(" + "));
627            out
628        }
629        Type::DynTrait(bounds) => {
630            let mut out = String::from("dyn ");
631            let parts: Vec<String> = bounds.iter().map(type_text).collect();
632            out.push_str(&parts.join(" + "));
633            out
634        }
635    }
636}
637
638fn literal_text(literal: &Literal) -> String {
639    match literal {
640        Literal::Int(text) | Literal::Float(text) => text.clone(),
641        Literal::Bool(value) => {
642            if *value {
643                "true".to_string()
644            } else {
645                "false".to_string()
646            }
647        }
648        Literal::Char(ch) => {
649            let mut out = String::from("'");
650            out.push_str(&escape_char(*ch));
651            out.push('\'');
652            out
653        }
654        Literal::Str(text) => {
655            let mut out = String::from("\"");
656            out.push_str(&escape_string(text));
657            out.push('"');
658            out
659        }
660        Literal::Bytes(bytes) => {
661            let mut out = String::from("&[");
662            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("{b}")).collect();
663            out.push_str(&parts.join(", "));
664            out.push(']');
665            out
666        }
667        Literal::Null => "None".to_string(),
668    }
669}
670
671/// An operand of a binary operation or of an `as`, parenthesised where
672/// Rust's precedence would otherwise re-associate it.
673///
674/// `rust::Expr` has no parenthesis node, so grouping is the renderer's
675/// and cannot be an emitter's to remember. `as` binds tighter than every
676/// binary operator, which made `bits >> 3 & 31 as u8` parse as
677/// `bits >> (3 & (31 as u8))` — a `u64` where the field is a `u8`, so
678/// the generated bitset decoder did not compile. A parenthesis never
679/// changes what an expression means, so this is added by shape rather
680/// than by a precedence table that could be wrong about a pair.
681///
682/// zerodds-lint: recursion-depth = as [`expr_text`], which this is one
683/// arm of.
684fn parenthesised(node: &Expr) -> String {
685    match node {
686        Expr::Binary { .. }
687        | Expr::Cast { .. }
688        | Expr::Closure { .. }
689        | Expr::If { .. }
690        | Expr::Match { .. }
691        | Expr::Assign { .. }
692        | Expr::Return(_) => {
693            let mut out = String::from("(");
694            out.push_str(&expr_text(node));
695            out.push(')');
696            out
697        }
698        _ => expr_text(node),
699    }
700}
701
702/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
703/// calls a routine per type rather than nesting one, so the one operand
704/// tree that grows with the IDL is an array's default value: one composite
705/// literal per dimension, built in `lang-rules/src/default_rust.rs`, and
706/// nothing bounds the dimension count. No number is claimed: this
707/// signature returns no error.
708#[allow(clippy::too_many_lines)]
709fn expr_text(node: &Expr) -> String {
710    match node {
711        Expr::Literal(literal) => literal_text(literal),
712        Expr::Path(path) => path_text(path),
713        Expr::MethodCall {
714            receiver,
715            method,
716            args,
717        } => {
718            let mut out = expr_text(receiver);
719            out.push('.');
720            out.push_str(method.as_str());
721            out.push('(');
722            let parts: Vec<String> = args.iter().map(expr_text).collect();
723            out.push_str(&parts.join(", "));
724            out.push(')');
725            out
726        }
727        Expr::Call { callee, args } => {
728            let mut out = expr_text(callee);
729            out.push('(');
730            let parts: Vec<String> = args.iter().map(expr_text).collect();
731            out.push_str(&parts.join(", "));
732            out.push(')');
733            out
734        }
735        Expr::StructLit { path, fields, base } => {
736            let mut out = path_text(path);
737            out.push_str(" { ");
738            let parts: Vec<String> = fields
739                .iter()
740                .map(|(name, value)| {
741                    // Field-init shorthand (`x` for `x: x`) wherever the
742                    // value is exactly the field's own name as a bare,
743                    // unrooted, one-segment path — `clippy::
744                    // redundant_field_names` denies the long form, and
745                    // every generated struct literal goes through this
746                    // one spot.
747                    if let Expr::Path(value_path) = value {
748                        if !value_path.is_rooted()
749                            && value_path.segments().len() == 1
750                            && value_path.last().map(Ident::as_str) == Some(name.as_str())
751                        {
752                            return name.as_str().to_string();
753                        }
754                    }
755                    let mut text = name.as_str().to_string();
756                    text.push_str(": ");
757                    text.push_str(&expr_text(value));
758                    text
759                })
760                .collect();
761            out.push_str(&parts.join(", "));
762            if let Some(base) = base {
763                if !fields.is_empty() {
764                    out.push_str(", ");
765                }
766                out.push_str("..");
767                out.push_str(&expr_text(base));
768            }
769            out.push_str(" }");
770            out
771        }
772        Expr::Field { receiver, name } => {
773            let mut out = expr_text(receiver);
774            out.push('.');
775            out.push_str(name.as_str());
776            out
777        }
778        Expr::Ref { target, mutable } => {
779            let mut out = String::from("&");
780            if *mutable {
781                out.push_str("mut ");
782            }
783            out.push_str(&expr_text(target));
784            out
785        }
786        Expr::Try(inner) => {
787            let mut out = expr_text(inner);
788            out.push('?');
789            out
790        }
791        Expr::Closure { params, body } => {
792            let mut out = String::from("|");
793            for (index, param) in params.iter().enumerate() {
794                if index > 0 {
795                    out.push_str(", ");
796                }
797                out.push_str(param.as_str());
798            }
799            out.push_str("| ");
800            out.push_str(&closure_body_text(body));
801            out
802        }
803        Expr::Match { scrutinee, arms } => {
804            // A match spans lines; rendered inline it would be one very
805            // long line, which is not what rustfmt produces.
806            let mut w = Writer::spaces4();
807            let mut head = String::from("match ");
808            head.push_str(&expr_text(scrutinee));
809            head.push_str(" {");
810            w.line(&head);
811            w.indent();
812            for arm in arms {
813                w.line(&arm_text(arm));
814            }
815            w.dedent();
816            w.push("}");
817            w.finish().trim_end().to_string()
818        }
819        Expr::If {
820            condition,
821            then,
822            otherwise,
823        } => {
824            let mut w = Writer::spaces4();
825            let mut head = String::from("if ");
826            head.push_str(&expr_text(condition));
827            head.push_str(" {");
828            w.line(&head);
829            w.indent();
830            render_block(&mut w, then);
831            w.dedent();
832            match otherwise {
833                // An else-block holding nothing but another `if` is an
834                // `else if`. Written as `} else { if .. }` it is the same
835                // program one level deeper, and `clippy::
836                // collapsible_else_if` in every crate that includes it:
837                // a `@mutable` decode is one such link per member.
838                Some(block) => match sole_if(block) {
839                    Some(inner) => {
840                        let mut tail = String::from("} else ");
841                        tail.push_str(&expr_text(inner));
842                        w.push(&tail);
843                    }
844                    None => {
845                        w.line("} else {");
846                        w.indent();
847                        render_block(&mut w, block);
848                        w.dedent();
849                        w.push("}");
850                    }
851                },
852                None => w.push("}"),
853            }
854            w.finish().trim_end().to_string()
855        }
856        Expr::For {
857            binding,
858            iterator,
859            body,
860        } => {
861            let mut w = Writer::spaces4();
862            let mut head = String::from("for ");
863            head.push_str(binding.as_str());
864            head.push_str(" in ");
865            head.push_str(&expr_text(iterator));
866            head.push_str(" {");
867            w.line(&head);
868            w.indent();
869            render_block(&mut w, body);
870            w.dedent();
871            w.push("}");
872            w.finish().trim_end().to_string()
873        }
874        Expr::While { condition, body } => {
875            let mut w = Writer::spaces4();
876            let mut head = String::from("while ");
877            head.push_str(&expr_text(condition));
878            head.push_str(" {");
879            w.line(&head);
880            w.indent();
881            render_block(&mut w, body);
882            w.dedent();
883            w.push("}");
884            w.finish().trim_end().to_string()
885        }
886        Expr::Assign { target, value } => {
887            let mut out = expr_text(target);
888            out.push_str(" = ");
889            out.push_str(&expr_text(value));
890            out
891        }
892        Expr::Return(value) => match value {
893            Some(value) => {
894                let mut out = String::from("return ");
895                out.push_str(&expr_text(value));
896                out
897            }
898            None => "return".to_string(),
899        },
900        Expr::Array(elements) => {
901            let mut out = String::from("[");
902            let parts: Vec<String> = elements.iter().map(expr_text).collect();
903            out.push_str(&parts.join(", "));
904            out.push(']');
905            out
906        }
907        Expr::Repeat { value, count } => {
908            let mut out = String::from("[");
909            out.push_str(&expr_text(value));
910            out.push_str("; ");
911            out.push_str(&expr_text(count));
912            out.push(']');
913            out
914        }
915        Expr::Tuple(elements) => {
916            let mut out = String::from("(");
917            let parts: Vec<String> = elements.iter().map(expr_text).collect();
918            out.push_str(&parts.join(", "));
919            if elements.len() == 1 {
920                out.push(',');
921            }
922            out.push(')');
923            out
924        }
925        Expr::Binary { op, lhs, rhs } => {
926            let mut out = parenthesised(lhs);
927            out.push(' ');
928            out.push_str(op.as_str());
929            out.push(' ');
930            out.push_str(&parenthesised(rhs));
931            out
932        }
933        Expr::Not(value) => {
934            let mut out = String::from("!");
935            out.push_str(&expr_text(value));
936            out
937        }
938        Expr::Deref(value) => {
939            let mut out = String::from("*");
940            out.push_str(&expr_text(value));
941            out
942        }
943        Expr::Cast { value, type_ } => {
944            let mut out = parenthesised(value);
945            out.push_str(" as ");
946            out.push_str(&type_text(type_));
947            out
948        }
949        Expr::Block(block) => {
950            let mut w = Writer::spaces4();
951            w.line("{");
952            w.indent();
953            render_block(&mut w, block);
954            w.dedent();
955            w.push("}");
956            w.finish().trim_end().to_string()
957        }
958    }
959}
960
961/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
962/// calls a routine per type rather than nesting one, so the one operand
963/// tree that grows with the IDL is an array's default value: one composite
964/// literal per dimension, built in `lang-rules/src/default_rust.rs`, and
965/// nothing bounds the dimension count. No number is claimed: this
966/// signature returns no error.
967fn arm_text(arm: &MatchArm) -> String {
968    let mut out = pattern_text(&arm.pattern);
969    if let Some(guard) = &arm.guard {
970        out.push_str(" if ");
971        out.push_str(&expr_text(guard));
972    }
973    out.push_str(" => ");
974    out.push_str(&expr_text(&arm.body));
975    out.push(',');
976    out
977}
978
979/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
980/// calls a routine per type rather than nesting one, so the one operand
981/// tree that grows with the IDL is an array's default value: one composite
982/// literal per dimension, built in `lang-rules/src/default_rust.rs`, and
983/// nothing bounds the dimension count. No number is claimed: this
984/// signature returns no error.
985fn pattern_text(pattern: &Pattern) -> String {
986    match pattern {
987        Pattern::Wildcard => "_".to_string(),
988        Pattern::Binding(name) => name.as_str().to_string(),
989        Pattern::Literal(literal) => literal_text(literal),
990        Pattern::Path(path) => path_text(path),
991        Pattern::TupleStruct { path, elements } => {
992            let mut out = path_text(path);
993            out.push('(');
994            let parts: Vec<String> = elements.iter().map(pattern_text).collect();
995            out.push_str(&parts.join(", "));
996            out.push(')');
997            out
998        }
999        Pattern::Struct { path, fields, rest } => {
1000            let mut out = path_text(path);
1001            out.push_str(" { ");
1002            let parts: Vec<String> = fields
1003                .iter()
1004                .map(|(name, pattern)| {
1005                    let mut text = name.as_str().to_string();
1006                    text.push_str(": ");
1007                    text.push_str(&pattern_text(pattern));
1008                    text
1009                })
1010                .collect();
1011            out.push_str(&parts.join(", "));
1012            if *rest {
1013                if !fields.is_empty() {
1014                    out.push_str(", ");
1015                }
1016                out.push_str("..");
1017            }
1018            out.push_str(" }");
1019            out
1020        }
1021    }
1022}