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}