Skip to main content

zerodds_lang_render/
cpp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the C++ CST. Spec ยง5.
4//!
5//! One style, the one the emitters this replaces wrote: four spaces, the
6//! opening brace on the same line, access labels at the class's own
7//! indentation, and a namespace body not indented โ€” which is what the
8//! fixture output shows.
9
10use crate::c::include_text;
11use crate::writer::{Writer, comment_lines, escape_char, escape_string, verbatim_lines};
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
16use zerodds_lang_cst::cpp::{
17    Alias, Base, Block, CastKind, Class, ClassMember, Constructor, Decl, Destructor, Enum,
18    Enumerator, Expr, Field, File, Function, FunctionBody, Method, Namespace, Param, Section,
19    Statement, SwitchCase, Template, TemplateParamKind, Type, Variable,
20};
21
22/// Render a whole file.
23#[must_use]
24pub fn file(file: &File) -> String {
25    let mut w = Writer::spaces4();
26    doc(&mut w, &file.doc);
27    if file.pragma_once {
28        w.line("#pragma once");
29        w.blank_line();
30    }
31    for include in &file.includes {
32        w.line(&include_text(include));
33    }
34    if !file.includes.is_empty() {
35        w.blank_line();
36    }
37    for (index, decl) in file.decls.iter().enumerate() {
38        if index > 0 {
39            w.blank_line();
40        }
41        render_decl(&mut w, decl);
42    }
43    w.finish()
44}
45
46/// Render one declaration, for a caller that has only one.
47#[must_use]
48pub fn decl(node: &Decl) -> String {
49    let mut w = Writer::spaces4();
50    render_decl(&mut w, node);
51    w.finish()
52}
53
54/// Render one type, for a caller that has only one.
55#[must_use]
56pub fn type_(node: &Type) -> String {
57    type_text(node)
58}
59
60/// Render one expression, for a caller that has only one.
61///
62/// What a generator building C++ by hand needs where the *value* is
63/// already a tree: the read harness's union arms compare against the
64/// same label the generated codec does, and the label's written form โ€”
65/// which suffix a wide one takes โ€” is `CodecTarget::wide_int`'s answer.
66/// Asking for the tree's text is how the two stay one rule rather than
67/// two that agree today.
68#[must_use]
69pub fn expr(node: &Expr) -> String {
70    expr_text(node)
71}
72
73/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
74/// module chain is cut at `MAX_MODULE_DEPTH` in
75/// `lang-rules/src/adapter/cpp.rs`)
76fn render_decl(w: &mut Writer, node: &Decl) {
77    match node {
78        Decl::Namespace(namespace) => render_namespace(w, namespace),
79        Decl::Class(class) => render_class(w, class),
80        Decl::Enum(e) => render_enum(w, e),
81        Decl::Alias(alias) => render_alias(w, alias),
82        Decl::Function(f) => render_function(w, f),
83        Decl::Variable(v) => render_variable(w, v),
84        Decl::Template(t) => render_template(w, t),
85        Decl::Comment(c) => doc(w, c),
86        Decl::Verbatim(text) => verbatim_lines(w, text),
87        Decl::Forward { is_struct, name } => {
88            let mut line = String::from(if *is_struct { "struct " } else { "class " });
89            line.push_str(name.as_str());
90            line.push(';');
91            w.line(&line);
92        }
93    }
94}
95
96/// A namespace body is not indented, which is what the fixture output
97/// shows and what keeps deeply nested modules readable.
98///
99/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
100/// module chain is cut at `MAX_MODULE_DEPTH` in
101/// `lang-rules/src/adapter/cpp.rs`)
102fn render_namespace(w: &mut Writer, node: &Namespace) {
103    doc(w, &node.doc);
104    let mut head = String::from("namespace ");
105    head.push_str(node.name.as_str());
106    head.push_str(" {");
107    w.line(&head);
108    for (index, decl) in node.decls.iter().enumerate() {
109        if index > 0 {
110            w.blank_line();
111        }
112        render_decl(w, decl);
113    }
114    let mut close = String::from("} // namespace ");
115    close.push_str(node.name.as_str());
116    w.line(&close);
117}
118
119fn render_class(w: &mut Writer, node: &Class) {
120    render_class_specialised(w, node, "");
121}
122
123/// A class, with the template arguments of an explicit specialisation
124/// written after its name.
125///
126/// `suffix` is `""` for an ordinary class and `<A, B>` for
127/// `template <> struct traits<A, B>`. It is threaded rather than baked
128/// into the name because a name is an [`Ident`] and `traits<::a::B>` is
129/// not one.
130///
131/// [`Ident`]: zerodds_lang_cst::common::Ident
132fn render_class_specialised(w: &mut Writer, node: &Class, suffix: &str) {
133    doc(w, &node.doc);
134    let mut head = String::from(if node.is_struct { "struct " } else { "class " });
135    head.push_str(node.name.as_str());
136    head.push_str(suffix);
137    if !node.bases.is_empty() {
138        head.push_str(" : ");
139        let parts: Vec<String> = node.bases.iter().map(base_text).collect();
140        head.push_str(&parts.join(", "));
141    }
142    head.push_str(" {");
143    w.line(&head);
144    for section in &node.sections {
145        render_section(w, section);
146    }
147    w.line("};");
148}
149
150fn base_text(base: &Base) -> String {
151    let mut out = String::from(base.access.as_str());
152    out.push(' ');
153    if base.virtual_ {
154        out.push_str("virtual ");
155    }
156    out.push_str(&path_text(&base.path));
157    out
158}
159
160/// The access label sits at the class's own indentation; its members are
161/// indented one level.
162fn render_section(w: &mut Writer, node: &Section) {
163    let mut label = String::from(node.access.as_str());
164    label.push(':');
165    w.line(&label);
166    w.indent();
167    for member in &node.members {
168        render_class_member(w, member);
169    }
170    w.dedent();
171    if !node.members.is_empty() {
172        w.blank_line();
173    }
174}
175
176fn render_class_member(w: &mut Writer, node: &ClassMember) {
177    match node {
178        ClassMember::Field(field) => render_field(w, field),
179        ClassMember::Method(method) => render_method(w, method),
180        ClassMember::Constructor(constructor) => render_constructor(w, constructor),
181        ClassMember::Destructor(destructor) => render_destructor(w, destructor),
182        ClassMember::Nested(decl) => render_decl(w, decl),
183        ClassMember::Alias(alias) => render_alias(w, alias),
184        ClassMember::Comment(c) => doc(w, c),
185        ClassMember::Verbatim(text) => verbatim_lines(w, text),
186    }
187}
188
189fn render_field(w: &mut Writer, node: &Field) {
190    doc(w, &node.doc);
191    let mut line = String::new();
192    if node.is_static {
193        line.push_str("static ");
194    }
195    if node.is_constexpr {
196        line.push_str("constexpr ");
197    }
198    line.push_str(&type_text(&node.type_));
199    line.push(' ');
200    line.push_str(node.name.as_str());
201    for dim in &node.dims {
202        line.push('[');
203        line.push_str(&expr_text(dim));
204        line.push(']');
205    }
206    if let Some(bits) = node.bits {
207        line.push_str(" : ");
208        line.push_str(&alloc::format!("{bits}"));
209    }
210    if let Some(default) = &node.default {
211        line.push_str(" = ");
212        line.push_str(&expr_text(default));
213    }
214    line.push(';');
215    w.line(&line);
216}
217
218fn render_method(w: &mut Writer, node: &Method) {
219    doc(w, &node.doc);
220    let mut head = String::new();
221    if node.is_static {
222        head.push_str("static ");
223    }
224    if node.is_virtual {
225        head.push_str("virtual ");
226    }
227    if node.is_constexpr {
228        head.push_str("constexpr ");
229    }
230    head.push_str(&type_text(&node.returns));
231    head.push(' ');
232    head.push_str(node.name.as_str());
233    head.push('(');
234    head.push_str(&params_text(&node.params));
235    head.push(')');
236    if node.is_const {
237        head.push_str(" const");
238    }
239    if node.is_noexcept {
240        head.push_str(" noexcept");
241    }
242    if node.is_override {
243        head.push_str(" override");
244    }
245    if node.is_pure {
246        head.push_str(" = 0;");
247        w.line(&head);
248        return;
249    }
250    render_body(w, head, node.body.as_ref());
251}
252
253/// A body that fits on one line stays on one line, which is what the
254/// accessors in the fixture output do.
255fn render_body(w: &mut Writer, mut head: String, body: Option<&Block>) {
256    let Some(block) = body else {
257        head.push(';');
258        w.line(&head);
259        return;
260    };
261    if let Some(single) = one_line_body(block) {
262        head.push_str(" { ");
263        head.push_str(&single);
264        head.push_str(" }");
265        w.line(&head);
266        return;
267    }
268    head.push_str(" {");
269    w.line(&head);
270    w.indent();
271    render_block(w, block);
272    w.dedent();
273    w.line("}");
274}
275
276/// The text of a body of exactly one simple statement.
277fn one_line_body(block: &Block) -> Option<String> {
278    match block.statements.as_slice() {
279        [Statement::Return(Some(value))] => {
280            let mut out = String::from("return ");
281            out.push_str(&expr_text(value));
282            out.push(';');
283            Some(out)
284        }
285        [Statement::Expr(value)] => {
286            let mut out = expr_text(value);
287            out.push(';');
288            Some(out)
289        }
290        _ => None,
291    }
292}
293
294fn render_constructor(w: &mut Writer, node: &Constructor) {
295    doc(w, &node.doc);
296    let mut head = String::new();
297    if node.is_explicit {
298        head.push_str("explicit ");
299    }
300    if node.is_constexpr {
301        head.push_str("constexpr ");
302    }
303    head.push_str(node.name.as_str());
304    head.push('(');
305    head.push_str(&params_text(&node.params));
306    head.push(')');
307    if node.is_noexcept {
308        head.push_str(" noexcept");
309    }
310
311    match &node.body {
312        FunctionBody::Defaulted => {
313            head.push_str(" = default;");
314            w.line(&head);
315        }
316        FunctionBody::Deleted => {
317            head.push_str(" = delete;");
318            w.line(&head);
319        }
320        FunctionBody::None => {
321            head.push(';');
322            w.line(&head);
323        }
324        FunctionBody::Block(block) => {
325            if node.initialisers.is_empty() {
326                render_body(w, head, Some(block));
327                return;
328            }
329            // The initialiser list goes on its own line, indented, which
330            // is the form the fixture output has.
331            w.line(&head);
332            w.indent();
333            let parts: Vec<String> = node
334                .initialisers
335                .iter()
336                .map(|(name, value)| {
337                    let mut text = name.as_str().to_string();
338                    text.push('(');
339                    text.push_str(&expr_text(value));
340                    text.push(')');
341                    text
342                })
343                .collect();
344            let mut line = String::from(": ");
345            line.push_str(&parts.join(", "));
346            if block.statements.is_empty() {
347                line.push_str(" {}");
348                w.line(&line);
349                w.dedent();
350                return;
351            }
352            line.push_str(" {");
353            w.line(&line);
354            w.indent();
355            render_block(w, block);
356            w.dedent();
357            w.line("}");
358            w.dedent();
359        }
360    }
361}
362
363fn render_destructor(w: &mut Writer, node: &Destructor) {
364    doc(w, &node.doc);
365    let mut head = String::new();
366    if node.is_virtual {
367        head.push_str("virtual ");
368    }
369    head.push('~');
370    head.push_str(node.name.as_str());
371    head.push_str("()");
372    match &node.body {
373        FunctionBody::Defaulted => {
374            head.push_str(" = default;");
375            w.line(&head);
376        }
377        FunctionBody::Deleted => {
378            head.push_str(" = delete;");
379            w.line(&head);
380        }
381        FunctionBody::None => {
382            head.push(';');
383            w.line(&head);
384        }
385        FunctionBody::Block(block) => render_body(w, head, Some(block)),
386    }
387}
388
389fn render_enum(w: &mut Writer, node: &Enum) {
390    doc(w, &node.doc);
391    let mut head = String::from(if node.is_scoped {
392        "enum class "
393    } else {
394        "enum "
395    });
396    head.push_str(node.name.as_str());
397    if let Some(underlying) = &node.underlying {
398        head.push_str(" : ");
399        head.push_str(&type_text(underlying));
400    }
401    head.push_str(" {");
402    w.line(&head);
403    w.indent();
404    for (index, enumerator) in node.enumerators.iter().enumerate() {
405        render_enumerator(w, enumerator, index + 1 == node.enumerators.len());
406    }
407    w.dedent();
408    w.line("};");
409}
410
411fn render_enumerator(w: &mut Writer, node: &Enumerator, last: bool) {
412    doc(w, &node.doc);
413    let mut line = String::from(node.name.as_str());
414    if let Some(value) = &node.value {
415        line.push_str(" = ");
416        line.push_str(&expr_text(value));
417    }
418    if !last {
419        line.push(',');
420    }
421    w.line(&line);
422}
423
424fn render_alias(w: &mut Writer, node: &Alias) {
425    doc(w, &node.doc);
426    let mut line = String::from("using ");
427    line.push_str(node.name.as_str());
428    line.push_str(" = ");
429    line.push_str(&type_text(&node.type_));
430    line.push(';');
431    w.line(&line);
432}
433
434fn render_function(w: &mut Writer, node: &Function) {
435    render_function_specialised(w, node, "");
436}
437
438/// A function, with the template arguments of an explicit
439/// specialisation written after its name. See
440/// [`render_class_specialised`].
441fn render_function_specialised(w: &mut Writer, node: &Function, suffix: &str) {
442    doc(w, &node.doc);
443    let mut head = String::new();
444    if node.is_maybe_unused {
445        head.push_str("[[maybe_unused]] ");
446    }
447    if node.is_static {
448        head.push_str("static ");
449    }
450    if node.is_inline {
451        head.push_str("inline ");
452    }
453    if node.is_constexpr {
454        head.push_str("constexpr ");
455    }
456    head.push_str(&type_text(&node.returns));
457    head.push(' ');
458    head.push_str(node.name.as_str());
459    head.push_str(suffix);
460    head.push('(');
461    head.push_str(&params_text(&node.params));
462    head.push(')');
463    if node.is_noexcept {
464        head.push_str(" noexcept");
465    }
466    match &node.body {
467        FunctionBody::Block(block) => render_body(w, head, Some(block)),
468        FunctionBody::Defaulted => {
469            head.push_str(" = default;");
470            w.line(&head);
471        }
472        FunctionBody::Deleted => {
473            head.push_str(" = delete;");
474            w.line(&head);
475        }
476        FunctionBody::None => {
477            head.push(';');
478            w.line(&head);
479        }
480    }
481}
482
483fn render_variable(w: &mut Writer, node: &Variable) {
484    doc(w, &node.doc);
485    let mut line = String::new();
486    if node.is_static {
487        line.push_str("static ");
488    }
489    if node.is_inline {
490        line.push_str("inline ");
491    }
492    if node.is_constexpr {
493        line.push_str("constexpr ");
494    }
495    line.push_str(&type_text(&node.type_));
496    line.push(' ');
497    line.push_str(node.name.as_str());
498    if let Some(value) = &node.value {
499        line.push_str(" = ");
500        line.push_str(&expr_text(value));
501    }
502    line.push(';');
503    w.line(&line);
504}
505
506/// zerodds-lint: recursion-depth 64 (one level per IDL module; the
507/// module chain is cut at `MAX_MODULE_DEPTH` in
508/// `lang-rules/src/adapter/cpp.rs`)
509fn render_template(w: &mut Writer, node: &Template) {
510    doc(w, &node.doc);
511    let mut head = String::from("template <");
512    let parts: Vec<String> = node
513        .params
514        .iter()
515        .map(|param| {
516            let mut text = match &param.kind {
517                TemplateParamKind::TypeName => "typename ".to_string(),
518                TemplateParamKind::Class => "class ".to_string(),
519                TemplateParamKind::NonType(type_) => {
520                    let mut out = type_text(type_);
521                    out.push(' ');
522                    out
523                }
524            };
525            text.push_str(param.name.as_str());
526            if let Some(default) = &param.default {
527                text.push_str(" = ");
528                text.push_str(&type_text(default));
529            }
530            text
531        })
532        .collect();
533    head.push_str(&parts.join(", "));
534    head.push('>');
535    w.line(&head);
536    if node.specialisation.is_empty() {
537        render_decl(w, &node.decl);
538        return;
539    }
540    // `Template::specialisation` used to be built and dropped here, so
541    // `template <> struct traits<::a::B>` came out as
542    // `template <> struct traits`, which is a redeclaration of the
543    // primary template and not a specialisation of it. Nothing in the
544    // tree built one until the CORBA Annex A.1.1 traits did.
545    let args: Vec<String> = node.specialisation.iter().map(type_text).collect();
546    let suffix = alloc::format!("<{}>", args.join(", "));
547    match node.decl.as_ref() {
548        Decl::Class(class) => render_class_specialised(w, class, &suffix),
549        Decl::Function(function) => render_function_specialised(w, function, &suffix),
550        // A namespace, an alias, an enumeration, a variable, a comment,
551        // a forward declaration and a nested template cannot carry
552        // explicit specialisation arguments in C++, so there is nothing
553        // to write them after and the declaration is rendered plain.
554        other => render_decl(w, other),
555    }
556}
557
558fn params_text(params: &[Param]) -> String {
559    let parts: Vec<String> = params
560        .iter()
561        .map(|param| {
562            let mut text = type_text(&param.type_);
563            if let Some(name) = &param.name {
564                text.push(' ');
565                text.push_str(name.as_str());
566            }
567            if let Some(default) = &param.default {
568                text.push_str(" = ");
569                text.push_str(&expr_text(default));
570            }
571            text
572        })
573        .collect();
574    parts.join(", ")
575}
576
577/// zerodds-lint: recursion-depth = the generated body's block nesting.
578/// The codec walk emits a routine per type rather than inlining one
579/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
580/// graph. What does grow with the IDL is the dispatch chain: a union's
581/// cases, an enumeration's enumerators and a `@mutable` aggregate's
582/// members each become one `If` per arm, in the previous arm's `else`.
583/// The model bounds none of the three counts, and C++ nests the arms in
584/// its own output too, so they cannot be walked iteratively the way
585/// `c.rs` walks them. No number is claimed: this signature returns no
586/// error, so nothing here could report one.
587fn render_block(w: &mut Writer, block: &Block) {
588    for statement in &block.statements {
589        render_statement(w, statement);
590    }
591}
592
593/// zerodds-lint: recursion-depth = the generated body's block nesting.
594/// The codec walk emits a routine per type rather than inlining one
595/// (`lang-rules/src/emit/encode.rs`), so nothing here follows the type
596/// graph. What does grow with the IDL is the dispatch chain: a union's
597/// cases, an enumeration's enumerators and a `@mutable` aggregate's
598/// members each become one `If` per arm, in the previous arm's `else`.
599/// The model bounds none of the three counts, and C++ nests the arms in
600/// its own output too, so they cannot be walked iteratively the way
601/// `c.rs` walks them. No number is claimed: this signature returns no
602/// error, so nothing here could report one.
603fn render_statement(w: &mut Writer, node: &Statement) {
604    match node {
605        Statement::Declare { type_, name, value } => {
606            let mut line = type_text(type_);
607            line.push(' ');
608            line.push_str(name.as_str());
609            if let Some(value) = value {
610                line.push_str(" = ");
611                line.push_str(&expr_text(value));
612            }
613            line.push(';');
614            w.line(&line);
615        }
616        Statement::Expr(value) => {
617            let mut line = expr_text(value);
618            line.push(';');
619            w.line(&line);
620        }
621        Statement::Return(value) => {
622            let mut line = String::from("return");
623            if let Some(value) = value {
624                line.push(' ');
625                line.push_str(&expr_text(value));
626            }
627            line.push(';');
628            w.line(&line);
629        }
630        Statement::If {
631            condition,
632            then,
633            otherwise,
634        } => {
635            let mut head = String::from("if (");
636            head.push_str(&expr_text(condition));
637            head.push_str(") {");
638            w.line(&head);
639            w.indent();
640            render_block(w, then);
641            w.dedent();
642            match otherwise {
643                Some(block) => {
644                    w.line("} else {");
645                    w.indent();
646                    render_block(w, block);
647                    w.dedent();
648                    w.line("}");
649                }
650                None => w.line("}"),
651            }
652        }
653        Statement::For {
654            init,
655            condition,
656            step,
657            body,
658        } => {
659            let mut head = String::from("for (");
660            head.push_str(statement_inline(init).trim_end_matches(';'));
661            head.push_str("; ");
662            head.push_str(&expr_text(condition));
663            head.push_str("; ");
664            head.push_str(&expr_text(step));
665            head.push_str(") {");
666            w.line(&head);
667            w.indent();
668            render_block(w, body);
669            w.dedent();
670            w.line("}");
671        }
672        Statement::While { condition, body } => {
673            let mut head = String::from("while (");
674            head.push_str(&expr_text(condition));
675            head.push_str(") {");
676            w.line(&head);
677            w.indent();
678            render_block(w, body);
679            w.dedent();
680            w.line("}");
681        }
682        Statement::Break => w.line("break;"),
683        Statement::ForEach {
684            binding,
685            range,
686            body,
687        } => {
688            let mut head = String::from("for (auto& ");
689            head.push_str(binding.as_str());
690            head.push_str(" : ");
691            head.push_str(&expr_text(range));
692            head.push_str(") {");
693            w.line(&head);
694            w.indent();
695            render_block(w, body);
696            w.dedent();
697            w.line("}");
698        }
699        Statement::Switch { value, cases } => {
700            let mut head = String::from("switch (");
701            head.push_str(&expr_text(value));
702            head.push_str(") {");
703            w.line(&head);
704            for case in cases {
705                render_case(w, case);
706            }
707            w.line("}");
708        }
709        Statement::Throw(value) => {
710            let mut line = String::from("throw ");
711            line.push_str(&expr_text(value));
712            line.push(';');
713            w.line(&line);
714        }
715        Statement::Try { body, handlers } => {
716            w.line("try {");
717            w.indent();
718            render_block(w, body);
719            w.dedent();
720            for handler in handlers {
721                let mut head = String::from("} catch (");
722                match &handler.type_ {
723                    Some(type_) => {
724                        head.push_str("const ");
725                        head.push_str(&type_text(type_));
726                        head.push('&');
727                        if let Some(name) = &handler.binding {
728                            head.push(' ');
729                            head.push_str(name.as_str());
730                        }
731                    }
732                    None => head.push_str("..."),
733                }
734                head.push_str(") {");
735                w.line(&head);
736                w.indent();
737                render_block(w, &handler.body);
738                w.dedent();
739            }
740            w.line("}");
741        }
742        Statement::Comment(c) => doc(w, c),
743    }
744}
745
746fn render_case(w: &mut Writer, case: &SwitchCase) {
747    if case.labels.is_empty() {
748        w.line("default:");
749    } else {
750        for label in &case.labels {
751            let mut line = String::from("case ");
752            line.push_str(&expr_text(label));
753            line.push(':');
754            w.line(&line);
755        }
756    }
757    w.indent();
758    render_block(w, &case.body);
759    if case.breaks {
760        w.line("break;");
761    }
762    w.dedent();
763}
764
765fn path_text(path: &Path) -> String {
766    let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
767    let joined = parts.join("::");
768    if path.is_rooted() {
769        let mut out = String::from("::");
770        out.push_str(&joined);
771        return out;
772    }
773    joined
774}
775
776/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
777/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
778/// `lang-rules/src/adapter/cpp.rs` refuses a type that nests deeper)
779fn type_text(node: &Type) -> String {
780    match node {
781        Type::Primitive(primitive) => primitive.as_str().to_string(),
782        Type::Named { path, args } => {
783            let mut out = path_text(path);
784            if !args.is_empty() {
785                out.push('<');
786                let parts: Vec<String> = args.iter().map(type_text).collect();
787                out.push_str(&parts.join(", "));
788                // `>>` closing two argument lists is one token in C++03
789                // and two in C++11; the target is C++17, so no space.
790                out.push('>');
791            }
792            out
793        }
794        Type::Pointer(target) => {
795            let mut out = type_text(target);
796            out.push('*');
797            out
798        }
799        Type::Ref(target) => {
800            let mut out = type_text(target);
801            out.push('&');
802            out
803        }
804        Type::RvalueRef(target) => {
805            let mut out = type_text(target);
806            out.push_str("&&");
807            out
808        }
809        Type::Const(target) => {
810            let mut out = String::from("const ");
811            out.push_str(&type_text(target));
812            out
813        }
814        Type::Array { element, length } => {
815            let mut out = type_text(element);
816            out.push('[');
817            out.push_str(&expr_text(length));
818            out.push(']');
819            out
820        }
821        Type::Void => "void".to_string(),
822        Type::Auto => "auto".to_string(),
823    }
824}
825
826/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
827/// calls a routine per type rather than nesting one, so the one operand
828/// tree that grows with the IDL is an array's default value: one composite
829/// literal per dimension, built in `lang-rules/src/default_cpp.rs`, and
830/// nothing bounds the dimension count. No number is claimed: this
831/// signature returns no error.
832#[allow(clippy::too_many_lines)]
833fn expr_text(node: &Expr) -> String {
834    match node {
835        Expr::Literal(literal) => literal_text(literal),
836        Expr::Path(path) => path_text(path),
837        Expr::Call { callee, args } => {
838            let mut out = expr_text(callee);
839            out.push('(');
840            let parts: Vec<String> = args.iter().map(expr_text).collect();
841            out.push_str(&parts.join(", "));
842            out.push(')');
843            out
844        }
845        Expr::MethodCall {
846            receiver,
847            name,
848            args,
849        } => {
850            let mut out = expr_text(receiver);
851            out.push('.');
852            out.push_str(name.as_str());
853            out.push('(');
854            let parts: Vec<String> = args.iter().map(expr_text).collect();
855            out.push_str(&parts.join(", "));
856            out.push(')');
857            out
858        }
859        Expr::Member { value, name } => {
860            let mut out = expr_text(value);
861            out.push('.');
862            out.push_str(name.as_str());
863            out
864        }
865        Expr::Arrow { value, name } => {
866            let mut out = expr_text(value);
867            out.push_str("->");
868            out.push_str(name.as_str());
869            out
870        }
871        Expr::Index { value, index } => {
872            let mut out = expr_text(value);
873            out.push('[');
874            out.push_str(&expr_text(index));
875            out.push(']');
876            out
877        }
878        Expr::Construct {
879            type_,
880            args,
881            braced,
882        } => {
883            let mut out = type_text(type_);
884            out.push(if *braced { '{' } else { '(' });
885            let parts: Vec<String> = args.iter().map(expr_text).collect();
886            out.push_str(&parts.join(", "));
887            out.push(if *braced { '}' } else { ')' });
888            out
889        }
890        Expr::Unary { op, value } => {
891            let mut out = String::from(*op);
892            out.push_str(&expr_text(value));
893            out
894        }
895        Expr::Binary { op, lhs, rhs } => {
896            let mut out = expr_text(lhs);
897            out.push(' ');
898            out.push_str(op);
899            out.push(' ');
900            out.push_str(&expr_text(rhs));
901            out
902        }
903        Expr::Cast { kind, type_, value } => {
904            let mut out = String::from(cast_text(*kind));
905            out.push('<');
906            out.push_str(&type_text(type_));
907            out.push_str(">(");
908            out.push_str(&expr_text(value));
909            out.push(')');
910            out
911        }
912        Expr::AddressOf(value) => {
913            let mut out = String::from("&");
914            out.push_str(&expr_text(value));
915            out
916        }
917        Expr::Deref(value) => {
918            let mut out = String::from("*");
919            out.push_str(&expr_text(value));
920            out
921        }
922        Expr::InitList(elements) => {
923            let mut out = String::from("{");
924            let parts: Vec<String> = elements.iter().map(expr_text).collect();
925            out.push_str(&parts.join(", "));
926            out.push('}');
927            out
928        }
929        Expr::SizeOf(type_) => {
930            let mut out = String::from("sizeof(");
931            out.push_str(&type_text(type_));
932            out.push(')');
933            out
934        }
935        Expr::Paren(inner) => {
936            let mut out = String::from("(");
937            out.push_str(&expr_text(inner));
938            out.push(')');
939            out
940        }
941    }
942}
943
944const fn cast_text(kind: CastKind) -> &'static str {
945    kind.as_str()
946}
947
948fn literal_text(literal: &Literal) -> String {
949    match literal {
950        Literal::Int(text) | Literal::Float(text) => text.clone(),
951        Literal::Bool(value) => {
952            if *value {
953                "true".to_string()
954            } else {
955                "false".to_string()
956            }
957        }
958        Literal::Char(ch) => {
959            let mut out = String::from("'");
960            out.push_str(&escape_char(*ch));
961            out.push('\'');
962            out
963        }
964        Literal::Str(text) => {
965            let mut out = String::from("\"");
966            out.push_str(&escape_string(text));
967            out.push('"');
968            out
969        }
970        Literal::Bytes(bytes) => {
971            let mut out = String::from("{");
972            let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
973            out.push_str(&parts.join(", "));
974            out.push('}');
975            out
976        }
977        Literal::Null => "nullptr".to_string(),
978    }
979}
980
981fn doc(w: &mut Writer, doc: &Doc) {
982    if doc.is_empty() {
983        return;
984    }
985    comment_lines(w, "// ", doc.as_lines());
986}
987
988/// A statement as a `for` clause holds it: no semicolon, no line.
989///
990/// Only the two forms a header can carry; anything else returns nothing
991/// rather than half a statement, which would put a silently empty
992/// initialiser in generated source.
993fn statement_inline(node: &Statement) -> String {
994    match node {
995        Statement::Declare { type_, name, value } => {
996            let mut out = type_text(type_);
997            out.push(' ');
998            out.push_str(name.as_str());
999            if let Some(value) = value {
1000                out.push_str(" = ");
1001                out.push_str(&expr_text(value));
1002            }
1003            out
1004        }
1005        Statement::Expr(value) => expr_text(value),
1006        _ => String::new(),
1007    }
1008}
1009
1010/// An integer as the source text of a C++ constant expression.
1011///
1012/// The same value C has no literal for, for the same reason one step
1013/// on: [lex.icon] gives an integer literal no sign, so
1014/// `-9223372036854775808LL` is a unary minus over `9223372036854775808`
1015/// with an `LL` suffix, and [lex.icon]/3 says a literal whose value
1016/// fits none of the types its suffix admits is ill-formed. Measured,
1017/// g++ 16.1.0 answers "integer constant is so large that it is
1018/// unsigned" and Apple clang 21.0.0 `-Wimplicitly-unsigned-literal`.
1019///
1020/// The subtraction is the one `<cstdint>` writes `INT64_MIN` as, and
1021/// `crates/wire-probe/src/corba_cpp.rs` and
1022/// `tools/wireprobe/wireprobe-zerodds-cpp-gen/src/fill.rs` already
1023/// spell the value that way.
1024#[must_use]
1025pub fn integer_text(value: i128) -> String {
1026    if value == i128::from(i64::MIN) {
1027        return "(-9223372036854775807LL - 1)".to_string();
1028    }
1029    format!("{value}")
1030}