Skip to main content

zerodds_lang_rules/
generate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! One IDL model into one language's source, through the contract the
5//! rest of this crate already keeps.
6//!
7//! The path is the seesaw one and nothing new:
8//!
9//! ```text
10//! Ast → ModelGraph → apply(model, rule::catalogue(language)) → Applied
11//!     → the language's materialiser → the language's renderer → text
12//! ```
13//!
14//! `rule::catalogue(Language)` is already the one door into the first
15//! half. This is the second half, written once instead of once per
16//! caller: every `tools/wireprobe/wireprobe-zerodds-<lang>-gen` spells
17//! the same four steps out again, which is why twenty-one of them
18//! disagree about what to call them — `lua_items` beside `nim_sections`
19//! beside `python_module`. The steps were never different; only the
20//! names were.
21//!
22//! What this does *not* decide is the framing. The caller passes a
23//! [`LayoutKey`], and a generated application passes
24//! `Dialect::Specification` — a dialect is what a measurement
25//! reproduces when it stands in for another vendor, and that belongs to
26//! the probes, not to a program someone ships.
27
28use zerodds_ast::Ast;
29use zerodds_ast_core::Id;
30use zerodds_ast_layout::Endianness;
31use zerodds_ast_layout::key::LayoutKey;
32use zerodds_lang_cst::Language;
33
34pub use crate::adapter::interface::ServiceMapping;
35use crate::emit::CodecTarget;
36use crate::emit::encode::CodecOptions;
37use crate::encode::EncodeError;
38use crate::{ModelGraph, apply, rule};
39
40/// Why a model did not become source.
41#[derive(Debug)]
42pub enum GenerateError {
43    /// The rules did not apply to the model.
44    Rules(String),
45    /// The language has no form for something in the model.
46    NoForm(String),
47    /// The language is materialised but not yet routed through here.
48    NotWired(Language),
49    /// The codec could not be built: the layout could not be planned, or
50    /// a construct has no writer or reader yet. [`source`]'s own error,
51    /// distinct from [`Self::NoForm`] because a materialiser's refusal
52    /// and a codec's are two different walks answering two different
53    /// questions about the same type.
54    Codec(String),
55}
56
57impl core::fmt::Display for GenerateError {
58    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59        match self {
60            Self::Rules(detail) => write!(f, "the rules do not apply: {detail}"),
61            Self::NoForm(detail) | Self::Codec(detail) => write!(f, "{detail}"),
62            Self::NotWired(language) => write!(
63                f,
64                "{language:?} has a materialiser but is not routed through `generate` yet"
65            ),
66        }
67    }
68}
69
70impl core::error::Error for GenerateError {}
71
72/// The languages whose `interface` binding is complete: every
73/// operation, every parameter direction, every raised exception and
74/// every attribute, with the request and reply topic types DDS-RPC 1.0
75/// §7.5.1.1 gives them where the binding has a codec at all. Every other
76/// language `refuse_interface`s an `interface` by name rather than let
77/// one reach a partial answer.
78///
79/// TypeScript, C++ and Java are `emit::interface_{typescript,cpp,
80/// java}`; C#, Go and Swift are `adapter::{csharp,go,swift}::
81/// interfaces` (all six flattened: OMG IDL 4.2 §7.4.3's inherited
82/// operations and attributes written directly into the one declaration,
83/// since none of them has a language mechanism this crate composes
84/// them through instead); Rust is `emit::interface_rust`, folded into
85/// [`source`] through `rust_source`, with a native supertrait. No
86/// language belongs here on the strength of an `adapter::<lang>` that
87/// merely does not refuse an interface node — `crates/lang-rules/tests/
88/// interface_bindings.rs`/`inherited_operations.rs` is where a language
89/// earns the list by measuring `raises` and an attribute, not only an
90/// operation's name.
91pub const COMPLETE_INTERFACE_LANGUAGES: [Language; 15] = [
92    Language::Ada,
93    Language::Cpp,
94    Language::CSharp,
95    Language::D,
96    Language::Elixir,
97    Language::Go,
98    Language::Java,
99    Language::Julia,
100    Language::Lua,
101    Language::Nim,
102    Language::OCaml,
103    Language::Rust,
104    Language::Swift,
105    Language::TypeScript,
106    Language::Zig,
107];
108
109/// [`COMPLETE_INTERFACE_LANGUAGES`], less Rust and Ada — what
110/// [`declarations`] itself answers `Ok` for.
111///
112/// `declarations` is the codec-less half of the pair: C++, Java, C#,
113/// Go, Nim, Swift, Zig and TypeScript were always declarations-only
114/// bindings — DDS-Cxx-PSM, the JVM/JS/.NET/Go/Nim/Swift/Zig runtimes
115/// give the octets a body outside this crate, which is `emit::
116/// interface_cpp`'s own module doc — so `declarations` was already
117/// their whole answer. Rust's and Ada's bindings are not: a `Requester`
118/// that cannot encode a request is not a binding, and both crates'
119/// codecs live in [`source`] (`rust_source`/`ada_source`) and nowhere
120/// in `declarations` — RM 7.2(4) is Ada's own reason, spelled out on
121/// [`source`]'s own two-Ada-columns paragraph. A model with an
122/// `interface`, asked of `declarations(ast, Language::Rust)` or
123/// `declarations(ast, Language::Ada)`, therefore refuses too — the
124/// complete answer exists, just not through this function.
125pub const COMPLETE_DECLARATIONS_LANGUAGES: [Language; 13] = [
126    Language::Cpp,
127    Language::CSharp,
128    Language::D,
129    Language::Elixir,
130    Language::Go,
131    Language::Java,
132    Language::Julia,
133    Language::Lua,
134    Language::Nim,
135    Language::OCaml,
136    Language::Swift,
137    Language::TypeScript,
138    Language::Zig,
139];
140
141/// `Err` when `ast` declares an `interface` and `language` is not in
142/// `complete` — DDS-TS-E002 and its own three failures aside, this is
143/// the fourth way an interface does not reach a language: the language
144/// itself (or, for [`declarations`], this particular entry point) has
145/// no complete binding for it. Naming every interface the model has and
146/// not only the first, so one refusal is not read as "the rest are
147/// fine".
148fn refuse_interface_unless(
149    ast: &Ast,
150    language: Language,
151    complete: &[Language],
152) -> Result<(), GenerateError> {
153    if complete.contains(&language) {
154        return Ok(());
155    }
156    let services = crate::adapter::interface::services(ast)
157        .map_err(|error| GenerateError::NoForm(error.to_string()))?;
158    if services.is_empty() {
159        return Ok(());
160    }
161    let supported = complete
162        .iter()
163        .copied()
164        .map(Language::as_str)
165        .collect::<Vec<_>>()
166        .join(", ");
167    let mut named = Vec::with_capacity(services.len());
168    for service in &services {
169        let node = ast
170            .interface(service.id)
171            .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
172        named.push(format!(
173            "`interface {}` at {}",
174            service.name,
175            interface_position(&node.facets.source)
176        ));
177    }
178    Err(GenerateError::NoForm(format!(
179        "{language:?} has no complete DDS-RPC 1.0 binding for an `interface`, and this model \
180         declares {}: {}. Generate it with one of: {supported}.",
181        named.len(),
182        named.join("; "),
183    )))
184}
185
186/// [`refuse_interface_unless`] against [`COMPLETE_INTERFACE_LANGUAGES`]
187/// — what [`source`] and [`csharp_source_with_runtime`] both check.
188fn refuse_interface(ast: &Ast, language: Language) -> Result<(), GenerateError> {
189    refuse_interface_unless(ast, language, &COMPLETE_INTERFACE_LANGUAGES)
190}
191
192/// `path:start-end` for a node's source span, or its origin in words
193/// where there is no span — a node the model API built rather than one
194/// a file parsed.
195fn interface_position(source: &zerodds_ast::facet::SourceFacet) -> String {
196    let where_ = match &source.origin {
197        zerodds_ast::facet::SourceOrigin::File { path } => path.clone(),
198        zerodds_ast::facet::SourceOrigin::Synthetic { reason } => {
199            format!("a synthetic node ({reason})")
200        }
201        zerodds_ast::facet::SourceOrigin::Builtin => "a built-in declaration".to_string(),
202    };
203    match source.span {
204        Some(span) => format!("{where_}:{}-{}", span.start, span.end),
205        None => where_,
206    }
207}
208
209/// The declarations of `ast`, as source in `language`.
210///
211/// # Errors
212/// [`GenerateError`], naming which of the three steps stopped. A model
213/// with an `interface` and a `language` outside
214/// [`COMPLETE_INTERFACE_LANGUAGES`] refuses before either of the other
215/// two runs — see `refuse_interface` — rather than answer with the
216/// interface silently missing from the output.
217pub fn declarations(ast: &Ast, language: Language) -> Result<String, GenerateError> {
218    declarations_with_mapping(ast, language, ServiceMapping::Basic)
219}
220
221/// [`declarations`] with the service mapping of DDS-RPC 1.0 §7.5.1 named.
222///
223/// [`ServiceMapping::Basic`] is the default of every other entry point;
224/// [`ServiceMapping::Enhanced`] writes the Enhanced topic types of
225/// §7.5.1.2 in the columns that have a binding for it (C++ and Java).
226///
227/// # Errors
228/// As [`declarations`].
229pub fn declarations_with_mapping(
230    ast: &Ast,
231    language: Language,
232    mapping: ServiceMapping,
233) -> Result<String, GenerateError> {
234    refuse_interface_unless(ast, language, &COMPLETE_DECLARATIONS_LANGUAGES)?;
235    let model = ModelGraph::of(ast);
236    let applied = apply(&model, &rule::catalogue(language))
237        .map_err(|error| GenerateError::Rules(format!("{error:?}")))?;
238
239    // One arm per language, and each is the pair the materialiser and
240    // the renderer were written as. The spellings differ because they
241    // were written apart; the shape does not.
242    //
243    // Three languages that otherwise have both halves of the pair —
244    // Ada83 (`adapter::ada83`/`render::ada83`), F# (`adapter::fsharp`/
245    // `render::fsharp`) and Kotlin (`adapter::kotlin`/`render::kotlin`)
246    // — have no arm here and cannot get one: `zerodds_lang_cst::Language`
247    // itself has no `Ada83`, `FSharp` or `Kotlin` variant, so `language`
248    // can never hold one of them and `rule::catalogue(language)` — the
249    // step before any of this — has nothing to dispatch to either. Their
250    // three `rule::<lang>::catalogue()` functions take no `Language` and
251    // are called directly, only from
252    // `crates/lang-rules/tests/{ada83,fsharp,kotlin}_end_to_end.rs`; that
253    // is a second, narrower contract this function does not go through.
254    let text = match language {
255        Language::Lua => {
256            let items = crate::adapter::lua::lua_items(ast, &applied)
257                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
258            items
259                .iter()
260                .map(zerodds_lang_render::lua::stat)
261                .collect::<Vec<_>>()
262                .join("\n")
263        }
264        Language::Ada => {
265            let items = crate::verbatim::weave_file(
266                ast,
267                crate::verbatim::aliases::ADA,
268                crate::adapter::ada::ada_items(ast, &applied)
269                    .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?,
270            );
271            items
272                .iter()
273                .map(zerodds_lang_render::ada::decl)
274                .collect::<Vec<_>>()
275                .join("\n")
276        }
277        Language::C => {
278            let items = crate::adapter::c::c_items(ast, &applied)
279                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
280            items
281                .iter()
282                .map(zerodds_lang_render::c::decl)
283                .collect::<Vec<_>>()
284                .join("\n")
285        }
286        Language::Cpp => {
287            let mut items = crate::adapter::cpp::cpp_items(ast, &applied)
288                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
289            // A service defined by a pair of types (DDS-RPC 1.0 §7.3.1.4):
290            // the common types of §7.5.1.1.1 are the runtime's, which comes
291            // first; the traits and the type support follow the structures.
292            let pair_items = crate::emit::type_pair_cpp::cpp_type_pair_items(ast)
293                .map_err(GenerateError::NoForm)?;
294            let has_pair_items = !pair_items.is_empty();
295            if has_pair_items {
296                crate::emit::type_pair_cpp::drop_common_declarations(&mut items);
297                items.insert(
298                    0,
299                    zerodds_lang_cst::cpp::Decl::Verbatim(
300                        crate::emit::type_pair_cpp::RUNTIME_INCLUDE.to_string(),
301                    ),
302                );
303                let aliases = crate::verbatim::aliases::CPP;
304                before_end_file(&mut items, ast, aliases, pair_items);
305            }
306            // Under `CppMapping::Psm` the declarations above are the native
307            // types, in their own namespace; the PSM classes follow and hand
308            // themselves to the codec through `zerodds::psm::Conv`. The RPC
309            // binding below spells the PSM's types where the application
310            // sees them (DDS-RPC 1.0 §7.11.1.1.2, DDS-PSM-Cxx 1.0 Table 7.1),
311            // so they have to be declared first.
312            if crate::adapter::cpp::cpp_mapping() == crate::adapter::cpp::CppMapping::Psm {
313                let layer = crate::adapter::cpp_psm::psm_layer(ast)
314                    .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
315                if !layer.is_empty() {
316                    before_end_file(
317                        &mut items,
318                        ast,
319                        crate::verbatim::aliases::CPP,
320                        vec![zerodds_lang_cst::cpp::Decl::Verbatim(layer)],
321                    );
322                }
323            }
324            // Under `CppMapping::Classic` the same, with the types of the classic
325            // OMG IDL to C++ mapping. The RPC binding names the PSM's types, so a
326            // model with an interface or a pair of types is refused.
327            if crate::adapter::cpp::cpp_mapping() == crate::adapter::cpp::CppMapping::Classic {
328                if has_pair_items {
329                    return Err(GenerateError::NoForm(
330                        "the classic C++ mapping has no form for the service types of DDS-RPC 1.0 §7.3.1.4"
331                            .to_string(),
332                    ));
333                }
334                let layer = crate::adapter::cpp_classic::classic_layer(ast)
335                    .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
336                before_end_file(
337                    &mut items,
338                    ast,
339                    crate::verbatim::aliases::CPP,
340                    vec![zerodds_lang_cst::cpp::Decl::Verbatim(layer)],
341                );
342            }
343            // The RPC binding of DDS-RPC 1.0 §7.11.1, after the data
344            // types: its topic structs name them.
345            before_end_file(
346                &mut items,
347                ast,
348                crate::verbatim::aliases::CPP,
349                crate::emit::interface_cpp::cpp_interface_items_with(ast, mapping)
350                    .map_err(|error| GenerateError::NoForm(error.to_string()))?,
351            );
352            items
353                .iter()
354                .map(zerodds_lang_render::cpp::decl)
355                .collect::<Vec<_>>()
356                .join("\n")
357        }
358        Language::CSharp => {
359            // C# has no `csharp_items`: only `csharp_file`, which is
360            // already the whole file and not a list to loop over.
361            let file = crate::adapter::csharp::csharp_file(ast, &applied)
362                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
363            zerodds_lang_render::csharp::file(&file)
364        }
365        Language::D => {
366            let items = crate::adapter::d::d_items(ast, &applied)
367                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
368            items
369                .iter()
370                .map(zerodds_lang_render::d::decl)
371                .collect::<Vec<_>>()
372                .join("\n")
373        }
374        Language::Elixir => {
375            // `elixir_modules`, not `elixir_file`, matching `lua_items`
376            // over `lua_file`: the modules only, without the chunk a
377            // generator writing straight to disk would wrap them in.
378            let modules = crate::adapter::elixir::elixir_modules(ast, &applied)
379                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
380            modules
381                .iter()
382                .map(zerodds_lang_render::elixir::top_level)
383                .collect::<Vec<_>>()
384                .join("\n")
385        }
386        Language::Go => {
387            let items = crate::adapter::go::go_items(ast, &applied)
388                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
389            items
390                .iter()
391                .map(zerodds_lang_render::go::decl)
392                .collect::<Vec<_>>()
393                .join("\n")
394        }
395        Language::Java => {
396            let mut items = crate::adapter::java::java_items(ast, &applied)
397                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
398            // A service defined by a pair of types (DDS-RPC 1.0 §7.3.1.4):
399            // the common types of §7.5.1.1.1 are the runtime's.
400            if crate::emit::type_pair_java::has_pair_types(ast) {
401                crate::emit::type_pair_java::adjust_declarations(&mut items);
402            }
403            // The RPC binding of DDS-RPC 1.0 §7.11.2, in its own outer
404            // class: `ZdTypes` holds the data, `ZdServices` the calls.
405            before_end_file(
406                &mut items,
407                ast,
408                crate::verbatim::aliases::JAVA,
409                crate::emit::interface_java::java_interface_items_with(ast, mapping)
410                    .map_err(|error| GenerateError::NoForm(error.to_string()))?,
411            );
412            items
413                .iter()
414                .map(zerodds_lang_render::java::type_decl)
415                .collect::<Vec<_>>()
416                .join("\n")
417        }
418        Language::Julia => {
419            let items = crate::adapter::julia::julia_items(ast, &applied)
420                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
421            items
422                .iter()
423                .map(zerodds_lang_render::julia::decl)
424                .collect::<Vec<_>>()
425                .join("\n")
426        }
427        Language::Nim => {
428            let sections = crate::adapter::nim::nim_sections(ast, &applied)
429                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
430            sections
431                .iter()
432                .map(zerodds_lang_render::nim::section)
433                .collect::<Vec<_>>()
434                .join("\n")
435        }
436        Language::Node => {
437            // `node_items` takes no `Applied`: `rule::catalogue` answers
438            // `Language::Node` with zero rules (see the module doc on
439            // `rule::catalogue`), because a struct, a union, a bitset, a
440            // valuetype, an exception and an interface are all plain
441            // object literals here and declare nothing a rule would
442            // compare.
443            let items = crate::adapter::node::node_items(ast)
444                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
445            items
446                .iter()
447                .map(zerodds_lang_render::node::stat)
448                .collect::<Vec<_>>()
449                .join("\n")
450        }
451        Language::OCaml => {
452            let items = crate::adapter::ocaml::ocaml_items(ast, &applied)
453                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
454            items
455                .iter()
456                .map(zerodds_lang_render::ocaml::decl)
457                .collect::<Vec<_>>()
458                .join("\n")
459        }
460        Language::Python => {
461            // Python has no `python_items`, only `python_module`.
462            let module = crate::adapter::python::python_module(ast, &applied)
463                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
464            zerodds_lang_render::python::module(&module)
465        }
466        Language::Rust => {
467            // Rust's materialiser lives in `adapter::cst`, not
468            // `adapter::rust` — there is no such module — because
469            // `adapter::cst` is the R-side of every rule, and Rust's own
470            // items are what `rust_items` folds out of it directly.
471            let items = crate::adapter::cst::rust_items(ast, &applied)
472                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
473            items
474                .iter()
475                .map(zerodds_lang_render::rust::item)
476                .collect::<Vec<_>>()
477                .join("\n")
478        }
479        Language::Swift => {
480            let items = crate::adapter::swift::swift_items(ast, &applied)
481                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
482            items
483                .iter()
484                .map(zerodds_lang_render::swift::decl)
485                .collect::<Vec<_>>()
486                .join("\n")
487        }
488        Language::TypeScript => {
489            // The two support types a generated file declares itself:
490            // the sixteen octets of a `long double` and the box an IDL
491            // `any` takes. Both are structural twins of the runtime's,
492            // so a value passes between them.
493            let mut items = crate::adapter::typescript::prelude();
494            // The data types with their descriptor side-tables. §2.1
495            // item 3 makes the side-tables part of the IDL-Codegen
496            // Profile — "for every IDL type it emits, it additionally
497            // emits a Type Descriptor and a corresponding type-guard
498            // function" — so they are not optional here. What they cost
499            // is the import below: a file carrying descriptors is no
500            // longer self-contained, because `registerType` is a
501            // function with process-wide state and no generated file can
502            // declare it.
503            items.extend(
504                crate::adapter::typescript::ts_items_with_descriptors(ast, &applied)
505                    .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?,
506            );
507            // The Operations Profile of DDS-TS 1.0 Ch11, after the data
508            // types: its descriptors name the exception descriptors the
509            // struct walk declares.
510            before_end_file(
511                &mut items,
512                ast,
513                crate::verbatim::aliases::TYPESCRIPT,
514                crate::emit::interface_typescript::ts_interface_items(ast, &applied)
515                    .map_err(|error| GenerateError::NoForm(error.to_string()))?,
516            );
517            zerodds_lang_render::typescript::file(&zerodds_lang_cst::typescript::File {
518                doc: zerodds_lang_cst::common::Doc::none(),
519                imports: vec![crate::adapter::typescript::descriptor_import(
520                    crate::adapter::typescript::DEFAULT_RUNTIME_PACKAGE,
521                )],
522                decls: items,
523            })
524        }
525        Language::Zig => {
526            let items = crate::adapter::zig::zig_items(ast, &applied)
527                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
528            items
529                .iter()
530                .map(zerodds_lang_render::zig::decl)
531                .collect::<Vec<_>>()
532                .join("\n")
533        }
534        Language::Ada83 => {
535            let items = crate::verbatim::weave_file(
536                ast,
537                crate::verbatim::aliases::ADA83,
538                crate::adapter::ada83::ada83_items(ast, &applied)
539                    .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?,
540            );
541            items
542                .iter()
543                .map(zerodds_lang_render::ada83::decl)
544                .collect::<Vec<_>>()
545                .join("\n")
546        }
547        Language::FSharp => {
548            let items = crate::adapter::fsharp::fsharp_items(ast, &applied)
549                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
550            items
551                .iter()
552                .map(zerodds_lang_render::fsharp::decl)
553                .collect::<Vec<_>>()
554                .join("\n")
555        }
556        Language::Kotlin => {
557            let items = crate::adapter::kotlin::kotlin_items(ast, &applied)
558                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
559            items
560                .iter()
561                .map(zerodds_lang_render::kotlin::decl)
562                .collect::<Vec<_>>()
563                .join("\n")
564        }
565    };
566    Ok(text)
567}
568
569/// Add `more` to `items` ahead of the `@verbatim(placement = END_FILE)`
570/// text the column put last: OMG IDL 4.2 §8.3.5.1 places that text at the
571/// end of the file, and the RPC declarations are part of the file.
572fn before_end_file<D>(items: &mut Vec<D>, ast: &Ast, aliases: &[&str], more: Vec<D>) {
573    let (_, end_file) = crate::verbatim::file_slots(ast, aliases);
574    let at = items.len().saturating_sub(end_file.len());
575    let tail = items.split_off(at);
576    items.extend(more);
577    items.extend(tail);
578}
579
580/// One Java compilation unit of a generated Java text.
581#[derive(Debug, Clone, PartialEq, Eq)]
582pub struct JavaUnit {
583    /// The public top-level class the unit declares, which is also the
584    /// stem of the file it has to be written to (`ZdTypes`,
585    /// `ZdServices`, `ZdCodec`).
586    pub class: &'static str,
587    /// The unit's text, ending in a newline.
588    pub text: String,
589}
590
591/// The generated Java text of [`source`] or [`declarations`] as the
592/// compilation units Java takes.
593///
594/// The generator writes `ZdTypes` (the data), `ZdServices` (the RPC
595/// binding of DDS-RPC 1.0 §7.11.2, when the model has an `interface`) and
596/// `ZdCodec` (the codec, for [`source`]) as top-level classes of one text,
597/// and JLS §7.6 allows one public top-level class per file, named as the
598/// file. This cuts the text where each class begins — a Javadoc block
599/// directly above a class goes with it — and returns one unit per class,
600/// in the order they stand. Whatever precedes the first class (a header
601/// comment, imports) stays with the first unit.
602#[must_use]
603pub fn java_units(text: &str) -> Vec<JavaUnit> {
604    const CLASSES: [(&str, &[&str]); 3] = [
605        ("ZdTypes", &["public final class ZdTypes {"]),
606        ("ZdServices", &["public final class ZdServices {"]),
607        ("ZdCodec", &["public class ZdCodec {"]),
608    ];
609    let mut starts: Vec<(usize, &'static str)> = Vec::new();
610    for (class, openings) in CLASSES {
611        let Some(at) = openings.iter().find_map(|opening| {
612            text.lines()
613                .scan(0usize, |offset, line| {
614                    let here = *offset;
615                    *offset += line.len() + 1;
616                    Some((here, line))
617                })
618                .find(|(_, line)| line == opening)
619                .map(|(here, _)| here)
620        }) else {
621            continue;
622        };
623        // A Javadoc block directly above belongs to the class.
624        let mut begin = at;
625        let mut lines_above: Vec<(usize, &str)> = text[..at]
626            .lines()
627            .scan(0usize, |offset, line| {
628                let here = *offset;
629                *offset += line.len() + 1;
630                Some((here, line))
631            })
632            .collect();
633        while let Some((here, line)) = lines_above.pop() {
634            let trimmed = line.trim_start();
635            if trimmed.starts_with("/**") || trimmed.starts_with('*') || trimmed.starts_with("*/") {
636                begin = here;
637                if trimmed.starts_with("/**") {
638                    break;
639                }
640            } else {
641                break;
642            }
643        }
644        starts.push((begin, class));
645    }
646    starts.sort_unstable();
647    let mut units = Vec::new();
648    for (index, (begin, class)) in starts.iter().enumerate() {
649        let from = if index == 0 { 0 } else { *begin };
650        let to = starts.get(index + 1).map_or(text.len(), |(next, _)| *next);
651        units.push(JavaUnit {
652            class,
653            text: format!("{}\n", text[from..to].trim_end()),
654        });
655    }
656    units
657}
658
659/// The declarations of `ast` and its codec, as source in `language`: the
660/// encoder [`crate::emit::encode::codec`] builds, the Table 9 defaulters
661/// its decoder calls, and the decoder [`crate::emit::decode::decoder`]
662/// builds — every one of them, rendered through the same materialiser
663/// and renderer [`declarations`] uses.
664///
665/// Every type the model declares, not one root. `codec` and `decoder`
666/// already walk and dedupe by name over the whole model — the same two
667/// functions `encode_c::c_codec` and `decode_c::c_decoder` bind for the
668/// C column, which `crates/security-rtps` builds a full binding through.
669/// A single `--type` root, the shape every `wireprobe-zerodds-<lang>-gen`
670/// takes, is a probe's question: it exists so a byte comparison has one
671/// sample to run, and a generated application has no such reason to stop
672/// at the first struct, union or valuetype the model declares.
673///
674/// # Errors
675/// [`GenerateError`]: [`GenerateError::Rules`] and
676/// [`GenerateError::NoForm`] as [`declarations`]; [`GenerateError::Codec`]
677/// when the layout cannot be planned or a construct has no writer or
678/// reader.
679pub fn source(ast: &Ast, language: Language, key: LayoutKey) -> Result<String, GenerateError> {
680    source_with_options(ast, language, key, CodecOptions::default())
681}
682
683/// As [`source`], with the one framing question [`LayoutKey`] does not
684/// answer named explicitly rather than defaulted.
685///
686/// A caller that reproduces a dialect — `tools/idlc generate --dialect`
687/// and `zerodds-build`'s own `Config::dialect` — has an answer for
688/// [`CodecOptions::map_length_with_delimiter`] the way it has one for
689/// every other axis: the dialect's, before any `--map-length` flag
690/// overrides it. [`source`] keeps [`CodecOptions::default`] for every
691/// caller that has no dialect to reproduce, which the module doc's
692/// "a generated application passes `Dialect::Specification`" is still
693/// true of.
694///
695/// # Errors
696/// As [`source`].
697pub fn source_with_options(
698    ast: &Ast,
699    language: Language,
700    key: LayoutKey,
701    options: CodecOptions,
702) -> Result<String, GenerateError> {
703    source_with_mapping(ast, language, key, options, ServiceMapping::Basic)
704}
705
706/// As [`source_with_options`], with the service mapping of DDS-RPC 1.0
707/// §7.5.1 named: the Basic mapping (the default of every other entry
708/// point) or the Enhanced one, whose topic types are `@Choice @Autoid`
709/// structures (§7.5.1.2).
710///
711/// # Errors
712/// As [`source`].
713pub fn source_with_mapping(
714    ast: &Ast,
715    language: Language,
716    key: LayoutKey,
717    options: CodecOptions,
718    mapping: ServiceMapping,
719) -> Result<String, GenerateError> {
720    // Ahead of the Rust/Ada/Ada83 special cases below: all three bypass
721    // `declarations`, which is where this refusal otherwise lives, and
722    // an interface must refuse the same way whichever path a language
723    // takes through this function.
724    refuse_interface(ast, language)?;
725    // Rust is the one language whose two halves cannot be two texts
726    // joined. Twenty backends emit free functions, so their codec is a
727    // block of declarations that can follow the types; Rust's is
728    // `impl Point`, which has to stand in the module `Point` was
729    // declared in. Both halves nest, and nesting them separately
730    // declares every shared ancestor twice — `nest_by_module`'s own doc
731    // is where that failure is written down: Rust allows a name once
732    // per scope, so `mod A { mod B { struct Inner } }` beside
733    // `mod A { mod B { impl Inner } }` is "the name `A` is defined
734    // multiple times". One fold, which is what `zerodds-build`'s
735    // `render_with_stem` does for the same reason.
736    //
737    // Rust's own codec (`crate::encode::rust_codec`) takes no
738    // `CodecOptions` at all — it has never grown the map-length branch
739    // the twenty `CodecTarget` backends share — so `options` has
740    // nothing to reach here and is not threaded through.
741    if language == Language::Rust {
742        return rust_source(ast, key, mapping);
743    }
744    // The two Ada columns are the other pair whose halves cannot be two
745    // texts joined, and for the opposite reason: Ada keeps a
746    // subprogram's profile and its body in two different compilation
747    // units. RM 7.1 admits only `basic_declarative_item`s in the
748    // visible part of a package specification, and a `subprogram_body`
749    // is not one — so a text that put the codec's bodies where its
750    // declarations are is not Ada at all. `gnatmake` says
751    // `compilation unit expected` at the first type declaration.
752    if language == Language::Ada {
753        return ada_source(ast, key, options);
754    }
755    if language == Language::Ada83 {
756        return ada83_source(ast, key, options);
757    }
758    // A C++ model with an interface or a pair of types has codecs in its
759    // declarations (the topic types of DDS-RPC 1.0 §7.5.1.1, the pair of
760    // §7.3.1.4). The file's own codec at the same scope would define the
761    // helpers they define, so it stands in a namespace of its own, as
762    // `cpp_source_with_mapping` puts it for `tools/idlc`.
763    if language == Language::Cpp
764        && (ast.interfaces().next().is_some() || crate::emit::type_pair_cpp::has_pair_types(ast))
765    {
766        return cpp_source_with_mapping(ast, key, options, "zd_codec", mapping).map(|c| c.text);
767    }
768    let decls = declarations_with_mapping(ast, language, mapping)?;
769    let codec = codec(ast, language, key, options)?;
770    // `@verbatim(placement = END_FILE)` closed the declarations, and the
771    // codec follows them in the same file; OMG IDL 4.2 §8.3.5.1 puts the
772    // text at the end of the file, so it moves past the codec.
773    let (decls, end_file) = split_end_file(ast, language, &decls)?;
774    Ok(format!("{decls}\n\n{codec}{end_file}"))
775}
776
777/// `decls` without the `END_FILE` text its column wrote last, and that
778/// text, each part as it will be written.
779///
780/// # Errors
781/// [`GenerateError::NoForm`] when the declarations do not end in the
782/// text: a column that wrote it elsewhere would have it moved by nothing,
783/// and a file whose `END_FILE` text stands in its middle is the
784/// misplacement this exists to prevent.
785fn split_end_file(
786    ast: &Ast,
787    language: Language,
788    decls: &str,
789) -> Result<(String, String), GenerateError> {
790    let (_, end_file) = crate::verbatim::file_slots(ast, crate::verbatim::aliases_of(language));
791    if end_file.is_empty() {
792        return Ok((decls.to_string(), String::new()));
793    }
794    let tail: Vec<&str> = end_file.iter().flat_map(|text| text.lines()).collect();
795    let tail = tail.join("\n");
796    let trimmed = decls.trim_end();
797    let Some(head) = trimmed.strip_suffix(tail.as_str()) else {
798        return Err(GenerateError::NoForm(format!(
799            "the {language:?} declarations do not end in their @verbatim END_FILE text, \
800             so it cannot be moved past the codec"
801        )));
802    };
803    Ok((head.trim_end().to_string(), format!("\n\n{tail}\n")))
804}
805
806/// The C++ source of `ast`, its codec inside `namespace codec_namespace`.
807///
808/// The codec is free functions at the scope it is written in, named
809/// after what they encode — `zd_enc_p_unsigned_long`, `zd_enc_string`,
810/// `zd_def_string` — so every file whose types use an `unsigned long`
811/// or a `string` writes the same names. At file scope, two generated
812/// headers in one translation unit define each of them twice, and the
813/// compiler stops at the second. In a namespace of the file's own, each
814/// header keeps its copy. The declarations stay where [`source`] puts
815/// them: their namespaces are the IDL modules'.
816///
817/// # Errors
818/// As [`source_with_options`].
819pub fn cpp_source_with_codec_namespace(
820    ast: &Ast,
821    key: LayoutKey,
822    options: CodecOptions,
823    codec_namespace: &str,
824) -> Result<String, GenerateError> {
825    cpp_source_with_both_versions(ast, key, options, codec_namespace).map(|both| both.text)
826}
827
828/// A C++ header that reads both XCDR versions.
829#[derive(Debug, Clone)]
830pub struct CppSource {
831    /// The header.
832    pub text: String,
833    /// The nested namespace the other version's codec stands in, and that
834    /// version — `None` when the model has no codec under it, in which
835    /// case the header reads the one version `key` names.
836    pub other: Option<(&'static str, zerodds_ast_layout::Encoding)>,
837}
838
839/// The C++ header with the codec `key` plans in `codec_namespace` and the
840/// other XCDR version's decoders beside it, in a nested namespace named for
841/// that version (`xcdr1` or `xcdr2`).
842///
843/// A peer chooses its representation (DDS-XTypes 1.3 §7.6.3.1.2) and a
844/// reader takes what arrives; one codec per file left a C++ reader
845/// refusing every sample a peer wrote in the other version. C++ keeps the
846/// two sets apart by namespace, so neither set is renamed: the routines of
847/// each call their own by unqualified name, and the
848/// `topic_type_support<T>::decode` the front end emits chooses by the
849/// version the encapsulation announces. The other version is planned with
850/// `key`'s every other axis — the dialect is one choice for the file.
851///
852/// # Errors
853/// As [`source`], for the version `key` names. A model the other version
854/// cannot carry is not an error: the header then holds one codec, and
855/// [`CppSource::other`] says so.
856pub fn cpp_source_with_both_versions(
857    ast: &Ast,
858    key: LayoutKey,
859    options: CodecOptions,
860    codec_namespace: &str,
861) -> Result<CppSource, GenerateError> {
862    cpp_source_with_mapping(ast, key, options, codec_namespace, ServiceMapping::Basic)
863}
864
865/// As [`cpp_source_with_both_versions`], with the service mapping of
866/// DDS-RPC 1.0 §7.5.1 named.
867///
868/// # Errors
869/// As [`cpp_source_with_both_versions`].
870pub fn cpp_source_with_mapping(
871    ast: &Ast,
872    key: LayoutKey,
873    options: CodecOptions,
874    codec_namespace: &str,
875    mapping: ServiceMapping,
876) -> Result<CppSource, GenerateError> {
877    use zerodds_ast_layout::Encoding;
878    refuse_interface(ast, Language::Cpp)?;
879    let decls = declarations_with_mapping(ast, Language::Cpp, mapping)?;
880    let primary = codec(ast, Language::Cpp, key, options)?;
881    let (decls, end_file) = split_end_file(ast, Language::Cpp, &decls)?;
882    let (nested, other_encoding) = if key.encoding.is_v2() {
883        ("xcdr1", Encoding::PlainCdr1)
884    } else {
885        ("xcdr2", Encoding::PlainCdr2)
886    };
887    let other_key = LayoutKey {
888        encoding: other_encoding,
889        ..key
890    };
891    // The decoders and the Table 9 defaulters they call, and no encoder:
892    // the trait writes in the version `key` names, so an encoder here
893    // would be a `static` routine nothing calls.
894    let other_decls = crate::emit::decode::decoder_excluding::<crate::emit::cpp::Cpp>(
895        ast,
896        other_key,
897        options,
898        &crate::emit::type_pair_cpp::excluded_types(ast),
899        &[],
900    )
901    .and_then(|decoders| {
902        let mut decls = crate::default_cpp::cpp_defaulters_called_by_excluding(
903            ast,
904            &decoders,
905            &crate::emit::type_pair_cpp::excluded_types(ast),
906        )?;
907        decls.extend(decoders);
908        Ok(decls)
909    })
910    .map(|decls| {
911        decls
912            .iter()
913            .map(zerodds_lang_render::cpp::decl)
914            .collect::<Vec<_>>()
915            .join("\n")
916    });
917    let (other_text, other) = match other_decls {
918        Ok(text) => (
919            format!(
920                "\n\n// The same types under the other XCDR version, which a peer may\n\
921                 // choose to write in. DDS-XTypes 1.3 \u{a7}7.6.3.1.2.\n\
922                 namespace {nested} {{\n\n{text}\n\n}} // namespace {nested}"
923            ),
924            Some((nested, other_encoding)),
925        ),
926        Err(_) => (String::new(), None),
927    };
928    // The PSM classes of `CppMapping::Psm` are among `decls`: the RPC binding
929    // that follows them names them.
930    Ok(CppSource {
931        text: format!(
932            "{decls}\n\nnamespace {codec_namespace} {{\n\n{primary}{other_text}\n\n}} // namespace {codec_namespace}{end_file}"
933        ),
934        other,
935    })
936}
937
938/// C# declarations and codecs linked against the shared wire runtime.
939///
940/// Unlike standalone declarations, these must use the runtime's IdlName and
941/// Float128 types, not redeclare same-named but assembly-incompatible copies.
942///
943/// # Errors
944/// The same rule, materialization and codec errors as [`source`].
945pub fn csharp_source_with_runtime(ast: &Ast, key: LayoutKey) -> Result<String, GenerateError> {
946    csharp_source_with_runtime_and_options(ast, key, CodecOptions::default())
947}
948
949/// As [`csharp_source_with_runtime`], with [`CodecOptions`] named
950/// explicitly. See [`source_with_options`].
951///
952/// # Errors
953/// As [`csharp_source_with_runtime`].
954pub fn csharp_source_with_runtime_and_options(
955    ast: &Ast,
956    key: LayoutKey,
957    options: CodecOptions,
958) -> Result<String, GenerateError> {
959    refuse_interface(ast, Language::CSharp)?;
960    let model = ModelGraph::of(ast);
961    let applied = apply(&model, &rule::catalogue(Language::CSharp))
962        .map_err(|error| GenerateError::Rules(format!("{error:?}")))?;
963    let mut file = crate::adapter::csharp::csharp_file(ast, &applied)
964        .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
965    crate::adapter::csharp::drop_support_namespace(&mut file);
966    let decls = zerodds_lang_render::csharp::file(&file);
967    let codec = codec(ast, Language::CSharp, key, options)?;
968    let (decls, end_file) = split_end_file(ast, Language::CSharp, &decls)?;
969    Ok(format!("{decls}\n\n{codec}{end_file}"))
970}
971
972/// The Ada spelling of the byte order `key` was planned under, as the
973/// constant a caller hands `Zd_Wire.New_Writer`.
974///
975/// Every other column takes this constant from `tools/idlc`'s preamble,
976/// which writes it above the declarations. Ada has no file scope to
977/// write it at: RM 3.1 makes an object declaration a declarative item,
978/// and a declarative item lives inside a compilation unit. So the
979/// constant is a declaration of the package like any other, and it is
980/// emitted here — where the package is — rather than spliced in from
981/// outside it.
982fn ada_endianness(endianness: Endianness) -> &'static str {
983    match endianness {
984        Endianness::Little => "Little",
985        Endianness::Big => "Big",
986    }
987}
988
989/// Ada's declarations and codec, as the two compilation units the
990/// language asks for: `package Zd_Types is …` and
991/// `package body Zd_Types is …`, in one text, in that order.
992///
993/// The split is by declaration kind and not by position: every
994/// `ada::Decl::SubprogramBody` the codec emits goes into the body, and
995/// everything else — the types, and the profile the emitter already
996/// writes for every one of those bodies — into the spec. That is what
997/// makes the codec reachable from outside: a subprogram whose body
998/// stands in a package body and whose profile stands nowhere is not
999/// declared by the package at all, which is the Ada form of the
1000/// `private static` the Java column was written as.
1001///
1002/// `tools/idlc` is what cuts the text into `zd_types.ads` and
1003/// `zd_types.adb` — the two file names GNAT derives the unit name from.
1004///
1005/// # Errors
1006/// As [`source`].
1007fn ada_source(ast: &Ast, key: LayoutKey, options: CodecOptions) -> Result<String, GenerateError> {
1008    use zerodds_lang_cst::ada;
1009    use zerodds_lang_cst::common::{Doc, Ident, Path};
1010
1011    let model = ModelGraph::of(ast);
1012    let applied = apply(&model, &rule::catalogue(Language::Ada))
1013        .map_err(|error| GenerateError::Rules(format!("{error:?}")))?;
1014    let mut spec = crate::adapter::ada::ada_items(ast, &applied)
1015        .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
1016    let codec = codec_of::<crate::emit::ada::Ada>(
1017        ast,
1018        key,
1019        options,
1020        crate::default_ada::ada_defaulters_called_by,
1021    )
1022    .map_err(codec_error)?;
1023    let mut bodies = Vec::new();
1024    for decl in codec {
1025        if matches!(decl, ada::Decl::SubprogramBody(_)) {
1026            bodies.push(decl);
1027        } else {
1028            spec.push(decl);
1029        }
1030    }
1031    // A model that emits no codec — an empty module, a file of nothing
1032    // but constants — gets neither the constant nor the body. RM 7.2(4)
1033    // allows a `package_body` only where the specification requires a
1034    // completion, and GNAT says `spec of this package does not allow a
1035    // body`; and the constant is what would name `Zd_Wire` in a unit
1036    // that otherwise never mentions the runtime, which is the same rule
1037    // `tools/idlc`'s preamble follows for every other column.
1038    if !bodies.is_empty() {
1039        spec.insert(
1040            0,
1041            ada::Decl::Object(ada::ObjectDecl {
1042                doc: Doc::lines(&[
1043                    "The byte order this unit was generated for. The codec below writes",
1044                    "either order -- Zd_Wire.Writer carries it -- so this is what a caller",
1045                    "hands Zd_Wire.New_Writer to get the one that was asked for.",
1046                ]),
1047                name: Ident::new("Zd_Endianness"),
1048                type_: Path::new(&["Zd_Wire", "Byte_Order"]),
1049                value: ada::Expr::Name(Path::new(&["Zd_Wire", ada_endianness(key.endianness)])),
1050            }),
1051        );
1052    }
1053    let package = Ident::new(crate::adapter::ada::PACKAGE);
1054    let spec_text = zerodds_lang_render::ada::file(&ada::File {
1055        doc: Doc::none(),
1056        withs: Vec::new(),
1057        use_types: Vec::new(),
1058        package: package.clone(),
1059        decls: spec,
1060    });
1061    // `@verbatim(placement = BEGIN_FILE)` before the specification —
1062    // where an Ada file's context clauses are — and `END_FILE` after the
1063    // last unit.
1064    let (begin_file, end_file) = crate::verbatim::file_slots(ast, crate::verbatim::aliases::ADA);
1065    let head = text_block(&begin_file, "\n");
1066    let tail = text_block(&end_file, "\n\n");
1067    if bodies.is_empty() {
1068        return Ok(format!("{head}{}\n{tail}", spec_text.trim_end()));
1069    }
1070    let body_text = zerodds_lang_render::ada::package_body(&package, &bodies);
1071    Ok(format!(
1072        "{head}{}\n\n{}\n{tail}",
1073        spec_text.trim_end(),
1074        body_text.trim_end()
1075    ))
1076}
1077
1078/// `@verbatim` text joined as the lines it is, followed by `separator`
1079/// — or nothing at all when there is none.
1080fn text_block(texts: &[String], separator: &str) -> String {
1081    if texts.is_empty() {
1082        return String::new();
1083    }
1084    let lines: Vec<&str> = texts.iter().flat_map(|text| text.lines()).collect();
1085    if separator == "\n" {
1086        format!("{}\n", lines.join("\n"))
1087    } else {
1088        format!("\n{}\n", lines.join("\n"))
1089    }
1090}
1091
1092/// Ada 83's declarations and codec, as the two compilation units the
1093/// language asks for. [`ada_source`] for Ada 2012, and the same split
1094/// for the same reason (RM83 7.1).
1095///
1096/// No endianness constant of `Zd_Wire83.Byte_Order` is missing here: it
1097/// is the first declaration of the package, exactly as the Ada 2012
1098/// column's is.
1099///
1100/// # Errors
1101/// As [`source`].
1102fn ada83_source(ast: &Ast, key: LayoutKey, options: CodecOptions) -> Result<String, GenerateError> {
1103    use zerodds_lang_cst::ada83;
1104    use zerodds_lang_cst::common::{Doc, Ident, Path};
1105
1106    let model = ModelGraph::of(ast);
1107    let applied = apply(&model, &rule::catalogue(Language::Ada83))
1108        .map_err(|error| GenerateError::Rules(format!("{error:?}")))?;
1109    let mut spec = crate::adapter::ada83::ada83_items(ast, &applied)
1110        .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
1111    let codec = codec_of::<crate::emit::ada83::Ada83>(
1112        ast,
1113        key,
1114        options,
1115        crate::default_ada83::ada83_defaulters_called_by,
1116    )
1117    .map_err(codec_error)?;
1118    let mut bodies = Vec::new();
1119    for decl in codec {
1120        if matches!(decl, ada83::Decl::SubprogramBody(_)) {
1121            bodies.push(decl);
1122        } else {
1123            spec.push(decl);
1124        }
1125    }
1126    // As [`ada_source`]: no codec, no body and no constant. RM83 7.1
1127    // and GNAT's `-gnat83` answer the same way.
1128    if !bodies.is_empty() {
1129        spec.insert(
1130            0,
1131            ada83::Decl::Object(ada83::ObjectDecl {
1132                doc: Doc::lines(&[
1133                    "The byte order this unit was generated for. The codec below writes",
1134                    "either order -- Zd_Wire83.Writer carries it -- so this is what a caller",
1135                    "hands Zd_Wire83.New_Writer to get the one that was asked for.",
1136                ]),
1137                name: Ident::new("Zd_Endianness"),
1138                type_: Path::new(&["Zd_Wire83", "Byte_Order"]),
1139                value: ada83::Expr::Name(Path::new(&["Zd_Wire83", ada_endianness(key.endianness)])),
1140            }),
1141        );
1142    }
1143    let package = Ident::new(crate::adapter::ada83::PACKAGE);
1144    let spec_text = zerodds_lang_render::ada83::file(&ada83::File {
1145        doc: Doc::none(),
1146        withs: Vec::new(),
1147        package: package.clone(),
1148        decls: spec,
1149    });
1150    // The file slots as in `ada_source`.
1151    let (begin_file, end_file) = crate::verbatim::file_slots(ast, crate::verbatim::aliases::ADA83);
1152    let head = text_block(&begin_file, "\n");
1153    let tail = text_block(&end_file, "\n\n");
1154    if bodies.is_empty() {
1155        return Ok(format!("{head}{}\n{tail}", spec_text.trim_end()));
1156    }
1157    let body_text = zerodds_lang_render::ada83::package_body(&package, &bodies);
1158    Ok(format!(
1159        "{head}{}\n\n{}\n{tail}",
1160        spec_text.trim_end(),
1161        body_text.trim_end()
1162    ))
1163}
1164
1165/// Rust's declarations and codec, folded into the module tree once.
1166///
1167/// Not `declarations` followed by `codec`: see [`source`]'s own comment.
1168/// What this fixes is not cosmetic. `impl Inner` was emitted at the top
1169/// level for an `Inner` declared in `module A { module B { … } }`, which
1170/// is `cannot find type `Inner` in this scope`, and every member path
1171/// inside the body was written relative to the module the item belongs
1172/// in — `super::A::B::Inner` read from the crate root, where `super`
1173/// leaves the crate. Placed in `A::B` and in `C` respectively, both
1174/// resolve.
1175///
1176/// # Errors
1177/// As [`source`].
1178fn rust_source(
1179    ast: &Ast,
1180    key: LayoutKey,
1181    mapping: ServiceMapping,
1182) -> Result<String, GenerateError> {
1183    let model = ModelGraph::of(ast);
1184    let applied = apply(&model, &rule::catalogue(Language::Rust))
1185        .map_err(|error| GenerateError::Rules(format!("{error:?}")))?;
1186    let mut pairs = crate::adapter::cst::rust_item_pairs(ast, &applied)
1187        .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
1188    pairs.extend(rust_codec_pairs(ast, key)?);
1189
1190    // `rust_item_pairs` still writes the flattened-trait answer the
1191    // codec-less `declarations()` entry point keeps — see its own
1192    // module doc and `crates/lang-rules/tests/inherited_operations.rs`
1193    // — which for a *remote* interface is now `emit::interface_rust`'s
1194    // job instead: two `pub trait I { .. }` in one module is
1195    // `error[E0428]`. Only a remote interface's trait is superseded —
1196    // `emit::interface_rust` writes none for a `local interface`
1197    // (`Service::is_remote`), so that one trait is the only answer and
1198    // stays exactly as `rust_item_pairs` wrote it.
1199    let remote_traits: std::collections::BTreeSet<String> =
1200        crate::adapter::interface::services(ast)
1201            .map_err(|error| GenerateError::NoForm(error.to_string()))?
1202            .into_iter()
1203            .filter(crate::adapter::interface::Service::is_remote)
1204            .map(|service| zerodds_lang_cst::rust::escape_keyword(&service.name))
1205            .collect();
1206    pairs.retain(|(_, item)| {
1207        !matches!(
1208            item,
1209            zerodds_lang_cst::rust::Item::Trait(trait_)
1210                if remote_traits.contains(trait_.name.as_str())
1211        )
1212    });
1213
1214    // The DDS-RPC 1.0 binding of every `interface`: the request/reply
1215    // topic types with a real XCDR2 codec, a client, a service trait
1216    // and a dispatcher over `zerodds_rpc`. Folded into the same pairs
1217    // and the same `nest_by_module` pass as everything else — see that
1218    // function's own doc on why a second fold is not an option.
1219    pairs.extend(
1220        crate::emit::interface_rust::rust_interface_pairs_with(ast, mapping)
1221            .map_err(|error| GenerateError::NoForm(error.to_string()))?,
1222    );
1223    // A service defined by a pair of types (DDS-RPC 1.0 §7.3.1.4): the
1224    // topic-type impls and the endpoints over them.
1225    pairs.extend(
1226        crate::emit::type_pair_rust::rust_type_pair_items(ast).map_err(GenerateError::NoForm)?,
1227    );
1228    let items = crate::verbatim::weave_file(
1229        ast,
1230        crate::verbatim::aliases::RUST,
1231        crate::adapter::cst::nest_by_module(ast, pairs)
1232            .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?,
1233    );
1234    Ok(render_rust(&items))
1235}
1236
1237/// Rust's codec items, each kept with the module its type was declared
1238/// in so a caller can fold them with items of its own.
1239///
1240/// `crate::encode::rust_codec` is called once per type the model
1241/// declares that gets one: a structure, a union, a valuetype, an
1242/// enumeration, a bitmask or a bitset. The list is `zerodds-build`'s
1243/// `has_codec` plus `Valuetype`, and the three it adds are not optional.
1244/// A `struct` with an `enum` member emits `self.col.encode(w)?` — the
1245/// member's own method — so a model that declares an enumeration and
1246/// never gets a codec for it is `no method named `encode` found for enum
1247/// `Colour``, measured, not a smaller output.
1248///
1249/// An anonymous type — `node.name.is_empty()` — is never one of these:
1250/// only a `sequence`, an `array` and a `map` synthesise one, and none of
1251/// the three is ever a top-level declaration.
1252///
1253/// # Errors
1254/// [`GenerateError::Codec`] when the layout cannot be planned or a
1255/// construct has no Rust writer.
1256fn rust_codec_pairs(
1257    ast: &Ast,
1258    key: LayoutKey,
1259) -> Result<Vec<(Id<zerodds_ast::ModuleNode>, zerodds_lang_cst::rust::Item)>, GenerateError> {
1260    let mut pairs = Vec::new();
1261    // A model with interfaces gets the XCDR1 pair of every type that has
1262    // one: DDS-RPC 1.0 §7.5.1.1, the Basic Service Mapping, is classic CDR,
1263    // and the topic types call the XCDR1 pair of the types they reach
1264    // (`emit::interface_rust`).
1265    let rpc = ast.interfaces().next().is_some() || crate::emit::type_pair_rust::has_pair_types(ast);
1266    for (id, node) in ast.types() {
1267        if node.name.is_empty() || !crate::encode::has_codec(&node.kind) {
1268            continue;
1269        }
1270        for item in crate::encode::rust_codec(ast, id, key).map_err(codec_error)? {
1271            pairs.push((node.module, item));
1272        }
1273        if rpc {
1274            if let Ok(items) = crate::encode::rust_codec_pair(
1275                ast,
1276                id,
1277                crate::emit::interface_rust::rpc_layout_key_xcdr1(),
1278            ) {
1279                for item in items {
1280                    pairs.push((node.module, item));
1281                }
1282            }
1283        }
1284    }
1285    Ok(pairs)
1286}
1287
1288/// `module.exports = { … };` over every function the Node column
1289/// declares.
1290///
1291/// CommonJS gives a module no other way to publish a name: a
1292/// `function` in a `.js` file is local to it, and
1293/// `require("./gen.js").zd_enc_Max` answered `undefined`. Nothing said
1294/// so, because the probe harness writes its own entry point into the
1295/// generated file rather than requiring it — the same blind spot the Go
1296/// and Zig columns had.
1297///
1298/// `None` where the run declared no function at all, so an empty model
1299/// does not gain a bare `module.exports = {}`.
1300fn node_exports(decls: &[zerodds_lang_cst::node::Stat]) -> Option<zerodds_lang_cst::node::Stat> {
1301    use zerodds_lang_cst::node::{Expr, Stat};
1302    let names: Vec<(zerodds_lang_cst::common::Ident, Expr)> = decls
1303        .iter()
1304        .filter_map(|decl| match decl {
1305            Stat::Function(function) => {
1306                Some((function.name.clone(), Expr::Name(function.name.clone())))
1307            }
1308            _ => None,
1309        })
1310        .collect();
1311    if names.is_empty() {
1312        return None;
1313    }
1314    Some(Stat::Assign {
1315        doc: zerodds_lang_cst::common::Doc::line(
1316            "What a caller outside this file can reach. CommonJS publishes nothing by default.",
1317        ),
1318        place: Expr::Member {
1319            object: Box::new(Expr::name("module")),
1320            name: zerodds_lang_cst::common::Ident::new("exports"),
1321        },
1322        value: Expr::Object(names),
1323    })
1324}
1325
1326/// Rust items as text, one blank line between them.
1327fn render_rust(items: &[zerodds_lang_cst::rust::Item]) -> String {
1328    items
1329        .iter()
1330        .map(zerodds_lang_render::rust::item)
1331        .collect::<Vec<_>>()
1332        .join("\n")
1333}
1334
1335/// [`EncodeError`] into [`GenerateError`], the one conversion every arm
1336/// below shares.
1337fn codec_error(error: EncodeError) -> GenerateError {
1338    GenerateError::Codec(format!("{error:?}"))
1339}
1340
1341/// The encoder, the Table 9 defaulters and the decoder for one
1342/// [`CodecTarget`], over the whole model and under one plan.
1343///
1344/// `defaulters` is the language's own `<lang>_defaulters_called_by`,
1345/// which reads the decoders it is handed to find which Table 9
1346/// defaulters they call. A parameter and not a method on `T`: the twenty
1347/// backends do not share one name for it, and `decode_c::c_decoder`
1348/// binds the same function this way for the same reason.
1349///
1350/// zerodds-lint: recursion-depth 1 — this does not recurse. The callee
1351/// is `crate::emit::encode::codec`; `dds_bounded_recursion` compares only the last
1352/// segment of a call path, so it reads as a self-call. The two
1353/// share a last segment and nothing else.
1354fn codec_of<T: CodecTarget>(
1355    ast: &Ast,
1356    key: LayoutKey,
1357    options: CodecOptions,
1358    defaulters: impl FnOnce(&Ast, &[T::Decl]) -> Result<Vec<T::Decl>, EncodeError>,
1359) -> Result<Vec<T::Decl>, EncodeError> {
1360    let mut decls = crate::emit::encode::codec::<T>(ast, key, options)?;
1361    let decoders = crate::emit::decode::decoder::<T>(ast, key, options)?;
1362    decls.extend(defaulters(ast, &decoders)?);
1363    decls.extend(decoders);
1364    Ok(decls)
1365}
1366
1367/// [`codec_of`] for C++, without the types the RPC binding of a pair of
1368/// types (DDS-RPC 1.0 §7.3.1.4) provides itself.
1369fn cpp_codec_of(
1370    ast: &Ast,
1371    key: LayoutKey,
1372    options: CodecOptions,
1373) -> Result<Vec<zerodds_lang_cst::cpp::Decl>, EncodeError> {
1374    use crate::emit::cpp::Cpp;
1375    let excluded = crate::emit::type_pair_cpp::excluded_types(ast);
1376    let mut decls = crate::emit::encode::codec_excluding::<Cpp>(ast, key, options, &excluded, &[])?;
1377    let decoders =
1378        crate::emit::decode::decoder_excluding::<Cpp>(ast, key, options, &excluded, &[])?;
1379    decls.extend(crate::default_cpp::cpp_defaulters_called_by_excluding(
1380        ast, &decoders, &excluded,
1381    )?);
1382    decls.extend(decoders);
1383    Ok(decls)
1384}
1385
1386/// [`codec_of`] for Java, without the types the RPC binding of a pair of
1387/// types (DDS-RPC 1.0 §7.3.1.4) provides itself and with the codecs of the
1388/// members of the pair types, which the binding calls.
1389fn java_codec_members(
1390    ast: &Ast,
1391    key: LayoutKey,
1392    options: CodecOptions,
1393) -> Result<Vec<zerodds_lang_cst::java::Member>, EncodeError> {
1394    use crate::emit::java::Java;
1395    let excluded = crate::emit::type_pair_java::excluded_types(ast);
1396    let mut reaching = crate::emit::type_pair_java::body_roots(ast);
1397    // The binding of an interface calls the codecs of the types its
1398    // operations carry.
1399    for id in crate::emit::interface_cpp::remote_member_types(ast).map_err(|error| {
1400        EncodeError::NoLayout {
1401            detail: error.to_string(),
1402        }
1403    })? {
1404        if !reaching.contains(&id) {
1405            reaching.push(id);
1406        }
1407    }
1408    let mut decls =
1409        crate::emit::encode::codec_excluding::<Java>(ast, key, options, &excluded, &reaching)?;
1410    let decoders =
1411        crate::emit::decode::decoder_excluding::<Java>(ast, key, options, &excluded, &reaching)?;
1412    decls.extend(crate::default_java::java_defaulters_called_by(
1413        ast, &decoders,
1414    )?);
1415    decls.extend(decoders);
1416    Ok(decls)
1417}
1418
1419/// [`java_codec_members`] and, when the model has a service, the same
1420/// codecs under classic CDR in the nested class `Xcdr1`: the Basic mapping
1421/// of DDS-RPC 1.0 §7.5.1.1 writes XCDR1 unless the params say XCDR2, and a
1422/// nested `@appendable` member has no DHEADER there while a `@mutable`
1423/// one is a parameter list (XTypes 1.3 §7.4.1.1, §7.4.1.2). The binding
1424/// calls `ZdCodec.Xcdr1.<name>` for the former and `ZdCodec.<name>` for
1425/// the latter.
1426fn java_codec_of(
1427    ast: &Ast,
1428    key: LayoutKey,
1429    options: CodecOptions,
1430) -> Result<Vec<zerodds_lang_cst::java::Member>, EncodeError> {
1431    use zerodds_lang_cst::java;
1432    let mut members = java_codec_members(ast, key, options)?;
1433    members.extend(crate::emit::type_pair_java::header_codecs(ast));
1434    if ast.interfaces().next().is_some() || crate::emit::type_pair_java::has_pair_types(ast) {
1435        let classic = LayoutKey {
1436            encoding: zerodds_ast_layout::Encoding::PlainCdr1,
1437            ..key
1438        };
1439        members.push(java::Member::Nested(Box::new(java::TypeDecl::Class(
1440            java::Class {
1441                doc: zerodds_lang_cst::common::Doc::line(
1442                    "The codecs of `ZdCodec` under classic CDR (XCDR1). DDS-RPC 1.0 \u{a7}7.5.1.1.",
1443                ),
1444                annotations: Vec::new(),
1445                modifiers: vec![java::Modifier::Public, java::Modifier::Static],
1446                name: zerodds_lang_cst::common::Ident::new("Xcdr1"),
1447                generics: Vec::new(),
1448                extends: None,
1449                implements: Vec::new(),
1450                members: java_codec_members(ast, classic, options)?,
1451            },
1452        ))));
1453    }
1454    Ok(members)
1455}
1456
1457/// Two spaces in front of every non-empty line.
1458///
1459/// `wireprobe-zerodds-elixir-gen`'s own helper: Elixir's codec functions
1460/// render at the top level and this is what nests them inside
1461/// `defmodule ZdCodec`.
1462fn indent(text: &str) -> String {
1463    let mut out = String::new();
1464    for line in text.lines() {
1465        if line.is_empty() {
1466            out.push('\n');
1467        } else {
1468            out.push_str("  ");
1469            out.push_str(line);
1470            out.push('\n');
1471        }
1472    }
1473    out
1474}
1475
1476/// The codec of `ast`, as source in `language`.
1477///
1478/// One arm per language, the same twenty-one [`declarations`] answers.
1479/// Fifteen share [`codec_of`] and a plain per-declaration render; four
1480/// more — C#, Java, Nim, Elixir — share [`codec_of`] but not the render,
1481/// for a reason each arm's comment gives. OCaml and Rust share neither:
1482/// OCaml's Table 9 defaulters are a distinct CST type from its codec
1483/// (`ocaml::ValueFn` beside `ocaml::CodecFn`), and Rust has no
1484/// [`CodecTarget`] at all — its codec is `crate::encode::rust_codec`,
1485/// walked directly against the Rust CST, which the module doc on
1486/// `crate::emit::encode` already gives the reason for: it "has not been
1487/// brought over". Every one of the twenty-one produces a full codec;
1488/// none is declarations-only.
1489///
1490/// zerodds-lint: recursion-depth 1 — this does not recurse. It calls
1491/// `codec_of`, and what `codec_of` calls back is
1492/// `crate::emit::encode::codec`, a different function that shares a
1493/// last segment with this one.
1494#[allow(clippy::too_many_lines)]
1495fn codec(
1496    ast: &Ast,
1497    language: Language,
1498    key: LayoutKey,
1499    options: CodecOptions,
1500) -> Result<String, GenerateError> {
1501    let text = match language {
1502        Language::Lua => {
1503            let decls = codec_of::<crate::emit::lua::Lua>(
1504                ast,
1505                key,
1506                options,
1507                crate::default_lua::lua_defaulters_called_by,
1508            )
1509            .map_err(codec_error)?;
1510            decls
1511                .iter()
1512                .map(zerodds_lang_render::lua::stat)
1513                .collect::<Vec<_>>()
1514                .join("\n")
1515        }
1516        Language::Ada => {
1517            let decls = codec_of::<crate::emit::ada::Ada>(
1518                ast,
1519                key,
1520                options,
1521                crate::default_ada::ada_defaulters_called_by,
1522            )
1523            .map_err(codec_error)?;
1524            decls
1525                .iter()
1526                .map(zerodds_lang_render::ada::decl)
1527                .collect::<Vec<_>>()
1528                .join("\n")
1529        }
1530        Language::C => {
1531            let decls = codec_of::<crate::emit::c::C>(
1532                ast,
1533                key,
1534                options,
1535                crate::default_c::c_defaulters_called_by,
1536            )
1537            .map_err(codec_error)?;
1538            decls
1539                .iter()
1540                .map(zerodds_lang_render::c::decl)
1541                .collect::<Vec<_>>()
1542                .join("\n")
1543        }
1544        Language::Cpp => {
1545            let decls = cpp_codec_of(ast, key, options).map_err(codec_error)?;
1546            decls
1547                .iter()
1548                .map(zerodds_lang_render::cpp::decl)
1549                .collect::<Vec<_>>()
1550                .join("\n")
1551        }
1552        // C# has no free functions: a codec is a `static` method, and a
1553        // method needs a class to live on. `ZdCodec` is
1554        // `wireprobe-zerodds-csharp-gen`'s own name for it.
1555        Language::CSharp => {
1556            let members = codec_of::<crate::emit::csharp::CSharp>(
1557                ast,
1558                key,
1559                options,
1560                crate::default_csharp::csharp_defaulters_called_by,
1561            )
1562            .map_err(codec_error)?;
1563            zerodds_lang_render::csharp::type_decl(&zerodds_lang_cst::csharp::TypeDecl::Class(
1564                zerodds_lang_cst::csharp::Class {
1565                    doc: zerodds_lang_cst::common::Doc::none(),
1566                    attributes: Vec::new(),
1567                    modifiers: vec![
1568                        zerodds_lang_cst::csharp::Modifier::Public,
1569                        zerodds_lang_cst::csharp::Modifier::Static,
1570                    ],
1571                    name: zerodds_lang_cst::common::Ident::new("ZdCodec"),
1572                    generics: Vec::new(),
1573                    bases: Vec::new(),
1574                    constraints: Vec::new(),
1575                    members,
1576                },
1577            ))
1578        }
1579        Language::D => {
1580            let decls = codec_of::<crate::emit::d::D>(
1581                ast,
1582                key,
1583                options,
1584                crate::default_d::d_defaulters_called_by,
1585            )
1586            .map_err(codec_error)?;
1587            decls
1588                .iter()
1589                .map(zerodds_lang_render::d::decl)
1590                .collect::<Vec<_>>()
1591                .join("\n")
1592        }
1593        // Elixir's codec functions render at the top level and Elixir
1594        // has no top level: every `def` needs a `defmodule` around it.
1595        // `wireprobe-zerodds-elixir-gen` writes exactly this module,
1596        // `import Bitwise` included, and this is that pattern with the
1597        // probe's harness and dump left out.
1598        Language::Elixir => {
1599            let decls = codec_of::<crate::emit::elixir::Elixir>(
1600                ast,
1601                key,
1602                options,
1603                crate::default_elixir::elixir_defaulters_called_by,
1604            )
1605            .map_err(codec_error)?;
1606            let mut functions = String::new();
1607            for function in &decls {
1608                functions.push('\n');
1609                functions.push_str(&indent(&zerodds_lang_render::elixir::codec_function(
1610                    function,
1611                )));
1612            }
1613            let mut out = String::from("defmodule ZdCodec do\n");
1614            if ["&&&", "|||", ">>>", "<<<"]
1615                .iter()
1616                .any(|operator| functions.contains(operator))
1617            {
1618                out.push_str("  import Bitwise\n");
1619            }
1620            out.push_str(&functions);
1621            out.push_str("end\n");
1622            out
1623        }
1624        Language::Go => {
1625            let decls = codec_of::<crate::emit::go::Go>(
1626                ast,
1627                key,
1628                options,
1629                crate::default_go::go_defaulters_called_by,
1630            )
1631            .map_err(codec_error)?;
1632            decls
1633                .iter()
1634                .map(zerodds_lang_render::go::decl)
1635                .collect::<Vec<_>>()
1636                .join("\n")
1637        }
1638        // Java, the same reason as C#: a codec is an instance-free
1639        // method and a method needs a class.
1640        Language::Java => {
1641            let members = java_codec_of(ast, key, options).map_err(codec_error)?;
1642            zerodds_lang_render::java::type_decl(&zerodds_lang_cst::java::TypeDecl::Class(
1643                zerodds_lang_cst::java::Class {
1644                    doc: zerodds_lang_cst::common::Doc::none(),
1645                    annotations: Vec::new(),
1646                    modifiers: vec![zerodds_lang_cst::java::Modifier::Public],
1647                    name: zerodds_lang_cst::common::Ident::new("ZdCodec"),
1648                    generics: Vec::new(),
1649                    extends: None,
1650                    implements: Vec::new(),
1651                    members,
1652                },
1653            ))
1654        }
1655        Language::Julia => {
1656            let decls = codec_of::<crate::emit::julia::Julia>(
1657                ast,
1658                key,
1659                options,
1660                crate::default_julia::julia_defaulters_called_by,
1661            )
1662            .map_err(codec_error)?;
1663            decls
1664                .iter()
1665                .map(zerodds_lang_render::julia::decl)
1666                .collect::<Vec<_>>()
1667                .join("\n")
1668        }
1669        // Nim's renderer joins a `seq` of procedures itself
1670        // (`proc_decls`, plural) rather than taking one at a time; every
1671        // other backend's renderer does not, and this is the one place
1672        // that shows.
1673        Language::Nim => {
1674            let decls = codec_of::<crate::emit::nim::Nim>(
1675                ast,
1676                key,
1677                options,
1678                crate::default_nim::nim_defaulters_called_by,
1679            )
1680            .map_err(codec_error)?;
1681            zerodds_lang_render::nim::proc_decls(&decls)
1682        }
1683        Language::Node => {
1684            let mut decls = codec_of::<crate::emit::node::Node>(
1685                ast,
1686                key,
1687                options,
1688                crate::default_node::node_defaulters_called_by,
1689            )
1690            .map_err(codec_error)?;
1691            if let Some(exports) = node_exports(&decls) {
1692                decls.push(exports);
1693            }
1694            decls
1695                .iter()
1696                .map(zerodds_lang_render::node::stat)
1697                .collect::<Vec<_>>()
1698                .join("\n")
1699        }
1700        // OCaml alone: its Table 9 defaulters are `ocaml::ValueFn`, a
1701        // type distinct from the codec's own `ocaml::CodecFn` — the one
1702        // backend whose `<lang>_defaulters_called_by` does not return
1703        // `Vec<T::Decl>`, so [`codec_of`] cannot bind it. `codec_chain`
1704        // and `value_chain` are `ocaml.rs`'s own renderers for the two;
1705        // the defaulters render ahead of the decoders that call them,
1706        // the same order `codec_of` gives every other backend.
1707        Language::OCaml => {
1708            let encoders =
1709                crate::emit::encode::codec::<crate::emit::ocaml::OCaml>(ast, key, options)
1710                    .map_err(codec_error)?;
1711            let decoders =
1712                crate::emit::decode::decoder::<crate::emit::ocaml::OCaml>(ast, key, options)
1713                    .map_err(codec_error)?;
1714            let defaulters = crate::default_ocaml::ocaml_defaulters_called_by(ast, &decoders)
1715                .map_err(codec_error)?;
1716            let mut out = zerodds_lang_render::ocaml::codec_chain(&encoders);
1717            out.push('\n');
1718            out.push_str(&zerodds_lang_render::ocaml::value_chain(&defaulters));
1719            out.push('\n');
1720            out.push_str(&zerodds_lang_render::ocaml::codec_chain(&decoders));
1721            out
1722        }
1723        Language::Python => {
1724            let decls = codec_of::<crate::emit::python::Python>(
1725                ast,
1726                key,
1727                options,
1728                crate::default_python::python_defaulters_called_by,
1729            )
1730            .map_err(codec_error)?;
1731            decls
1732                .iter()
1733                .map(zerodds_lang_render::python::statement)
1734                .collect::<Vec<_>>()
1735                .join("\n")
1736        }
1737        // Rust has no `CodecTarget` backend — see this function's own
1738        // doc — so its codec is `crate::encode::rust_codec`, gathered by
1739        // [`rust_codec_pairs`] and folded into the module tree here:
1740        // `impl Inner` belongs in the module `Inner` was declared in,
1741        // and the member paths in its body are written relative to that
1742        // module.
1743        //
1744        // [`source`] does not come through this arm. It folds these
1745        // pairs together with the declarations' in one pass, because two
1746        // separate folds declare every shared ancestor twice. This arm
1747        // is the codec on its own — correct in isolation, which is what
1748        // a caller asking for only the codec gets.
1749        Language::Rust => {
1750            let items = crate::adapter::cst::nest_by_module(ast, rust_codec_pairs(ast, key)?)
1751                .map_err(|error| GenerateError::NoForm(format!("{error:?}")))?;
1752            render_rust(&items)
1753        }
1754        Language::Swift => {
1755            let decls = codec_of::<crate::emit::swift::Swift>(
1756                ast,
1757                key,
1758                options,
1759                crate::default_swift::swift_defaulters_called_by,
1760            )
1761            .map_err(codec_error)?;
1762            decls
1763                .iter()
1764                .map(zerodds_lang_render::swift::decl)
1765                .collect::<Vec<_>>()
1766                .join("\n")
1767        }
1768        Language::TypeScript => {
1769            let decls = codec_of::<crate::emit::typescript::TypeScript>(
1770                ast,
1771                key,
1772                options,
1773                crate::default_typescript::typescript_defaulters_called_by,
1774            )
1775            .map_err(codec_error)?;
1776            decls
1777                .iter()
1778                .map(zerodds_lang_render::typescript::decl)
1779                .collect::<Vec<_>>()
1780                .join("\n")
1781        }
1782        Language::Zig => {
1783            let decls = codec_of::<crate::emit::zig::Zig>(
1784                ast,
1785                key,
1786                options,
1787                crate::default_zig::zig_defaulters_called_by,
1788            )
1789            .map_err(codec_error)?;
1790            decls
1791                .iter()
1792                .map(zerodds_lang_render::zig::decl)
1793                .collect::<Vec<_>>()
1794                .join("\n")
1795        }
1796        Language::Ada83 => {
1797            let decls = codec_of::<crate::emit::ada83::Ada83>(
1798                ast,
1799                key,
1800                options,
1801                crate::default_ada83::ada83_defaulters_called_by,
1802            )
1803            .map_err(codec_error)?;
1804            decls
1805                .iter()
1806                .map(zerodds_lang_render::ada83::decl)
1807                .collect::<Vec<_>>()
1808                .join("\n")
1809        }
1810        Language::FSharp => {
1811            let decls = codec_of::<crate::emit::fsharp::Fsharp>(
1812                ast,
1813                key,
1814                options,
1815                crate::default_fsharp::fsharp_defaulters_called_by,
1816            )
1817            .map_err(codec_error)?;
1818            decls
1819                .iter()
1820                .map(zerodds_lang_render::fsharp::decl)
1821                .collect::<Vec<_>>()
1822                .join("\n")
1823        }
1824        Language::Kotlin => {
1825            let decls = codec_of::<crate::emit::kotlin::Kotlin>(
1826                ast,
1827                key,
1828                options,
1829                crate::default_kotlin::kotlin_defaulters_called_by,
1830            )
1831            .map_err(codec_error)?;
1832            decls
1833                .iter()
1834                .map(zerodds_lang_render::kotlin::decl)
1835                .collect::<Vec<_>>()
1836                .join("\n")
1837        }
1838    };
1839    Ok(text)
1840}