Skip to main content

zerodds_lang_rules/adapter/
cst.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The language CST as the other end of a rule. Spec §3.2.
4//!
5//! A rule's R-side creates seesaw nodes; this materialises them as CST
6//! nodes. Creation is total: a seesaw node whose type has no CST
7//! counterpart is an error at apply time, not a node silently dropped.
8//!
9//! Values are resolved from the model, never from the overlay. A rule
10//! that carried a name through the graph would put that name into a hash
11//! and make renaming a member change the identity of everything
12//! downstream. Spec §2.3.
13
14use crate::adapter::constant;
15use crate::apply::Applied;
16use seesaw_tgg::ident::GhostId;
17use zerodds_ast::{Ast, PrimitiveKind, TypeKind, TypeNode};
18use zerodds_ast_core::{Id, NodeId};
19use zerodds_lang_cst::common::{Doc, Ident, Path, Visibility};
20use zerodds_lang_cst::rust;
21
22/// Why a created node could not be materialised.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum MaterialiseError {
25    /// A created node's type has no CST counterpart.
26    UnknownType {
27        /// The type name the rule created.
28        type_name: String,
29    },
30    /// A created node has no model node behind it.
31    ///
32    /// Every CST node needs its values, and the values come from the
33    /// model; a node with no origin has nowhere to get them.
34    NoOrigin {
35        /// The type name of the node.
36        type_name: String,
37    },
38    /// The module an item nests inside cannot be read from the model.
39    ///
40    /// Reported rather than absorbed. A module id the arena does not
41    /// answer for, or a parent chain deeper than `MAX_MODULE_DEPTH`,
42    /// used to return the items **unwrapped** — `NameComponent` at the
43    /// crate root instead of inside `pub mod CosNaming`. The scope is
44    /// part of the declaration: `module a { struct P; };` and
45    /// `module b { struct P; };` are two types, and flattening them is
46    /// the same class of substitution as writing the wrong octet.
47    NoModule {
48        /// The module handle, as far as it can be named without the
49        /// node the model would not hand over.
50        at: String,
51    },
52    /// A child the model listed and then would not hand over.
53    ///
54    /// A walk asks the model for a type's members, cases, enumerators or
55    /// bits and then reads each one by its handle. A handle that does
56    /// not resolve means the model contradicts itself, and the only
57    /// honest answer is to say so: skipping the child emits an
58    /// aggregate with a field missing, a union with an arm missing or an
59    /// enumeration with a value missing, and reports success. Nothing
60    /// downstream can tell such a type from one the source declared that
61    /// way — it is the same class of substitution as writing the wrong
62    /// octet, and it reaches the wire.
63    NoNode {
64        /// What was asked for: `"member"`, `"case"`, `"enumerator"`,
65        /// `"bitfield"`, `"bit"`, `"operation"` or `"parameter"`.
66        kind: &'static str,
67        /// Where it sits, as far as the model names it.
68        at: String,
69    },
70    /// A value the output has to state does not evaluate.
71    ///
72    /// An array length, a constant's value, an enumerator's value or a
73    /// bit's position. Substituting a zero would put a plausible number
74    /// in the generated source — a `[T; 0]` where an array belongs, or
75    /// two enumerators sharing a discriminant — and nothing downstream
76    /// could tell it from a number the source asked for.
77    Unevaluated {
78        /// What could not be worked out, in the source's terms.
79        construct: String,
80        /// Where it sits, as far as the model names it.
81        at: String,
82    },
83    /// A construct the model holds and the target language cannot.
84    ///
85    /// Named rather than skipped. A generator that left the member out
86    /// would emit a type missing a field and report success; one that
87    /// wrote the construct's IDL spelling into the output would emit a
88    /// file no compiler reads and report success. Both are the same
89    /// failure and this is the third answer.
90    NoLanguageForm {
91        /// The construct, in the source's terms.
92        construct: String,
93        /// The language that has no form for it.
94        language: &'static str,
95        /// Where it sits, as far as the model names it.
96        at: String,
97    },
98    /// Two declarations in one scope spell the same name in the target.
99    ///
100    /// Every column escapes a name that collides with a keyword of its
101    /// language by appending an underscore, so an IDL `int` becomes a C
102    /// `int_`. A scope that already declares `int_` then has two
103    /// members with one name, and the escape has silently made one of
104    /// them unreachable.
105    ///
106    /// What each column does with that today is not the same, and none
107    /// of it names the cause: the C compiler says `duplicate member
108    /// 'int_'`, Python says `keyword argument repeated`, and a column
109    /// whose target permits the duplicate says nothing at all — the
110    /// second declaration wins and one member is dropped from a file
111    /// that compiles. The escape is the only layer that holds both
112    /// spellings at once, which is why the check is here and not in
113    /// seven backends.
114    SpellingCollision {
115        /// The two IDL names, in declaration order.
116        first: String,
117        /// The second, which escaped onto the first.
118        second: String,
119        /// The spelling they share.
120        spelled: String,
121        /// The language whose keyword forced the escape.
122        language: &'static str,
123        /// The scope holding both.
124        at: String,
125    },
126}
127
128impl core::fmt::Display for MaterialiseError {
129    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
130        match self {
131            Self::NoNode { kind, at } => {
132                write!(f, "{at}: the model lists a {kind} it will not hand over")
133            }
134            Self::Unevaluated { construct, at } => {
135                write!(f, "{at}: the {construct} does not evaluate")
136            }
137            Self::NoLanguageForm {
138                construct,
139                language,
140                at,
141            } => write!(f, "{at}: {language} has no form for `{construct}`"),
142            Self::UnknownType { type_name } => {
143                write!(
144                    f,
145                    "the rules created a `{type_name}`, which is not a CST node"
146                )
147            }
148            Self::NoOrigin { type_name } => {
149                write!(f, "a created `{type_name}` has no model node behind it")
150            }
151            Self::SpellingCollision {
152                first,
153                second,
154                spelled,
155                language,
156                at,
157            } => write!(
158                f,
159                "{at}: `{first}` and `{second}` both spell `{spelled}` in \
160                 {language}, so one of them would be unreachable"
161            ),
162            Self::NoModule { at } => {
163                write!(
164                    f,
165                    "the module {at} is not in the model, so the items it \
166                     declares have no scope to nest in"
167                )
168            }
169        }
170    }
171}
172
173impl core::error::Error for MaterialiseError {}
174
175/// The names a scope declares, checked for a collision the escape made.
176///
177/// `spelled` is what the target writes, `declared` is what the IDL
178/// said. A column calls this once per scope, before it builds the
179/// members, and gets the pair back rather than a file with a name
180/// missing.
181///
182/// Order matters and is the model's: the error names the earlier
183/// declaration first, so two runs over one model report the same pair.
184///
185/// # Errors
186/// [`MaterialiseError::SpellingCollision`] when two declared names
187/// reach one spelling.
188pub fn no_spelling_collision<'a>(
189    names: impl IntoIterator<Item = (&'a str, &'a str)>,
190    language: &'static str,
191    at: &str,
192) -> Result<(), MaterialiseError> {
193    let mut seen: Vec<(&str, &str)> = Vec::new();
194    for (spelled, declared) in names {
195        if let Some((_, first)) = seen.iter().find(|(other, _)| *other == spelled) {
196            return Err(MaterialiseError::SpellingCollision {
197                first: (*first).to_string(),
198                second: declared.to_string(),
199                spelled: spelled.to_string(),
200                language,
201                at: at.to_string(),
202            });
203        }
204        seen.push((spelled, declared));
205    }
206    Ok(())
207}
208
209/// Build the Rust items a rule application produced.
210///
211/// The order is the model's own declaration order, so two runs over one
212/// model produce the same file.
213///
214/// # Errors
215/// [`MaterialiseError`] when a created node has no counterpart or no
216/// origin — never a silently dropped node.
217pub fn rust_items(ast: &Ast, applied: &Applied) -> Result<Vec<rust::Item>, MaterialiseError> {
218    Ok(crate::verbatim::weave_file(
219        ast,
220        crate::verbatim::aliases::RUST,
221        nest_by_module(ast, rust_item_pairs(ast, applied)?)?,
222    ))
223}
224
225/// Weave every type's `@verbatim` text into the items the type walk
226/// declared. `starts` holds, per type in walk order, where its items
227/// begin; they end where the next type's begin. Woven last to first, so
228/// an index still to be used never moves.
229///
230/// `BEGIN_DECLARATION` and `END_DECLARATION` open and close the body of
231/// the type's `struct`; an enum and a type alias take them adjacent.
232fn weave_rust_verbatim(
233    ast: &Ast,
234    starts: &[(Id<TypeNode>, Id<zerodds_ast::ModuleNode>, usize)],
235    items: &mut Vec<(Id<zerodds_ast::ModuleNode>, rust::Item)>,
236) {
237    let mut end = items.len();
238    for (id, module, start) in starts.iter().rev() {
239        let slots = crate::verbatim::declaration_slots(ast, *id, crate::verbatim::aliases::RUST);
240        let range_end = end;
241        end = *start;
242        if slots.is_empty() {
243            continue;
244        }
245        let name = ast
246            .type_(*id)
247            .map(|node| rust::escape_keyword(&node.name))
248            .unwrap_or_default();
249        let tail = items.split_off(range_end.min(items.len()));
250        let module = *module;
251        crate::verbatim::weave_with(
252            slots,
253            *start,
254            items,
255            |text| (module, rust::Item::Verbatim(text)),
256            |declared, begin, end| {
257                for (_, item) in declared {
258                    if crate::verbatim::Carrier::take_into_body(item, &name, begin, end) {
259                        return;
260                    }
261                }
262            },
263        );
264        items.extend(tail);
265    }
266}
267
268/// `@verbatim` in the Rust column: `BEGIN_DECLARATION` and
269/// `END_DECLARATION` open and close a struct's braced body; an enum and a
270/// type alias take them adjacent.
271impl crate::verbatim::Carrier for rust::Item {
272    fn verbatim(text: String) -> Self {
273        Self::Verbatim(text)
274    }
275
276    fn take_into_body(
277        &mut self,
278        name: &str,
279        begin: &mut Vec<String>,
280        end: &mut Vec<String>,
281    ) -> bool {
282        let Self::Struct(node) = self else {
283            return false;
284        };
285        if node.name.as_str() != name || matches!(node.body, rust::StructBody::Tuple(_)) {
286            return false;
287        }
288        node.body_text.begin.append(begin);
289        node.body_text.end.append(end);
290        true
291    }
292}
293
294/// As [`rust_items`], each item paired with the module it belongs in and
295/// nothing nested yet.
296///
297/// A caller that has items of its own for the same model — a build
298/// script emitting codecs beside the types — must fold **one** list.
299/// Folding two and concatenating declares every shared ancestor twice,
300/// and Rust allows a name once per scope.
301///
302/// # Errors
303/// As [`rust_items`].
304pub fn rust_item_pairs(
305    ast: &Ast,
306    applied: &Applied,
307) -> Result<Vec<(Id<zerodds_ast::ModuleNode>, rust::Item)>, MaterialiseError> {
308    // Each item is kept with the module that declared it, and the
309    // nesting is folded in at the end.
310    //
311    // **This used to be a flat `Vec<rust::Item>`, and the nesting was
312    // dropped.** The catalogue has had a `module-to-mod` rule from the
313    // start; the rule fires, the correspondence is created, and nothing
314    // here ever turned it into a `rust::Item::Module` — so
315    // `module a { struct P; }; module b { struct P; };` materialised as
316    // two `pub struct P` in one scope, and every scoped name a consumer
317    // wrote resolved to the wrong path or to nothing. Found by
318    // `crates/corba-interop`, whose IDL declares `module CosNaming` and
319    // whose consumers import `CosNaming::NameComponent`.
320    let mut items: Vec<(Id<zerodds_ast::ModuleNode>, rust::Item)> = Vec::new();
321
322    // Where each type's items begin, for the `@verbatim` pass after the
323    // walk: the arms below leave by `continue` at many points, so the
324    // text is woven once every type has declared what it declares.
325    let mut starts: Vec<(Id<TypeNode>, Id<zerodds_ast::ModuleNode>, usize)> = Vec::new();
326
327    // Walk the model rather than the graph: the model has the order the
328    // source declared, and the graph has no order to speak of.
329    for (id, node) in ast.types() {
330        // Every type this item names is written relative to the module the
331        // item itself lands in.
332        let from = node.module;
333        starts.push((id, from, items.len()));
334        match &node.kind {
335            TypeKind::Enumeration { enumerators } => {
336                if ghost_created_for(applied, node.node_id, "RustEnum").is_none() {
337                    continue;
338                }
339                let mut variants = Vec::new();
340                for enumerator in enumerators {
341                    let Ok(enum_node) = ast.enumerator(*enumerator) else {
342                        continue;
343                    };
344                    if ghost_created_for(applied, enum_node.node_id, "RustVariant").is_none() {
345                        continue;
346                    }
347                    // The value is the effective one, written out: a
348                    // Rust enum without discriminants would renumber
349                    // silently when a variant is inserted, and the wire
350                    // format is the IDL's numbering, not Rust's.
351                    let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
352                        MaterialiseError::Unevaluated {
353                            construct: "enumerator value".to_string(),
354                            at: enum_node.name.clone(),
355                        }
356                    })?;
357                    variants.push(rust::Variant {
358                        doc: Doc::none(),
359                        attrs: Vec::new(),
360                        name: Ident::new(&rust::escape_keyword(&enum_node.name)),
361                        body: rust::VariantBody::Unit,
362                        discriminant: Some(rust::Expr::Literal(
363                            zerodds_lang_cst::common::Literal::int(value),
364                        )),
365                    });
366                }
367                items.push((
368                    node.module,
369                    rust::Item::Enum(rust::Enum {
370                        doc: Doc::none(),
371                        attrs: Vec::new(),
372                        // `Debug`, `Clone` and `PartialEq` and nothing
373                        // else. The rule this file states — a derive is a
374                        // decision about the target language and not about
375                        // the wire — is about `Default`, whose Rust answer
376                        // and Table 9's part company at an enumeration
377                        // whose first literal is not zero. These three make
378                        // no claim about the wire at all, and without them
379                        // a generated type cannot be printed in an
380                        // assertion, compared with another, or held twice.
381                        // `crates/corba-interop`'s harness needs all three,
382                        // and `Eq` is not among them because a `double`
383                        // member rules it out.
384                        derives: data_derives(),
385                        visibility: Visibility::Public,
386                        name: Ident::new(&rust::escape_keyword(&node.name)),
387                        generics: Vec::new(),
388                        variants,
389                    }),
390                ));
391                continue;
392            }
393            TypeKind::Alias { aliased } => {
394                if ghost_created_for(applied, node.node_id, "RustTypeAlias").is_none() {
395                    continue;
396                }
397                // An anonymous alias is one the frontend made for an
398                // inline type; it has no name to declare.
399                if node.name.is_empty() {
400                    continue;
401                }
402                items.push((
403                    node.module,
404                    rust::Item::TypeAlias(rust::TypeAlias {
405                        doc: Doc::none(),
406                        attrs: Vec::new(),
407                        visibility: Visibility::Public,
408                        name: Ident::new(&rust::escape_keyword(&node.name)),
409                        generics: Vec::new(),
410                        type_: rust_type_of(ast, from, aliased.resolved())?,
411                    }),
412                ));
413                continue;
414            }
415            TypeKind::Union { cases, .. } => {
416                if ghost_created_for(applied, node.node_id, "RustUnionEnum").is_none() {
417                    continue;
418                }
419                // A union becomes an enum with one payload variant per
420                // case: Rust's untagged union is unsafe, and the tag is
421                // what the discriminator already is. Where the case does
422                // not fix the discriminator, the variant holds it ahead
423                // of the member, and a discriminator that may select no
424                // member has a variant of its own (`crate::rust_union`).
425                let shape = crate::rust_union::shape(ast, id).map_err(|reason| {
426                    MaterialiseError::NoLanguageForm {
427                        construct: reason.to_string(),
428                        language: "rust",
429                        at: node.name.clone(),
430                    }
431                })?;
432                let discriminator_type = rust_type_of(ast, from, Some(shape.discriminator))?;
433                let mut variants = Vec::new();
434                for case in cases {
435                    let Ok(case_node) = ast.case(*case) else {
436                        continue;
437                    };
438                    if ghost_created_for(applied, case_node.node_id, "RustCaseVariant").is_none() {
439                        continue;
440                    }
441                    let Ok(member) = ast.member(case_node.member) else {
442                        continue;
443                    };
444                    let carries = shape
445                        .cases
446                        .iter()
447                        .find(|c| c.member.node_id == member.node_id)
448                        .is_some_and(crate::rust_union::RustCase::carries_discriminator);
449                    let mut fields = Vec::new();
450                    if carries {
451                        fields.push(discriminator_type.clone());
452                    }
453                    fields.push(field_type(ast, from, member)?);
454                    variants.push(rust::Variant {
455                        doc: Doc::none(),
456                        attrs: Vec::new(),
457                        name: Ident::new(&variant_name(&member.name)),
458                        body: rust::VariantBody::Tuple(fields),
459                        discriminant: None,
460                    });
461                }
462                if shape.no_member {
463                    variants.push(rust::Variant {
464                        doc: Doc::none(),
465                        attrs: Vec::new(),
466                        name: Ident::new(crate::rust_union::NO_MEMBER),
467                        body: rust::VariantBody::Tuple(vec![discriminator_type]),
468                        discriminant: None,
469                    });
470                }
471                items.push((
472                    node.module,
473                    rust::Item::Enum(rust::Enum {
474                        doc: Doc::none(),
475                        attrs: Vec::new(),
476                        // `Debug`, `Clone` and `PartialEq` and nothing
477                        // else. The rule this file states — a derive is a
478                        // decision about the target language and not about
479                        // the wire — is about `Default`, whose Rust answer
480                        // and Table 9's part company at an enumeration
481                        // whose first literal is not zero. These three make
482                        // no claim about the wire at all, and without them
483                        // a generated type cannot be printed in an
484                        // assertion, compared with another, or held twice.
485                        // `crates/corba-interop`'s harness needs all three,
486                        // and `Eq` is not among them because a `double`
487                        // member rules it out.
488                        derives: data_derives(),
489                        visibility: Visibility::Public,
490                        name: Ident::new(&rust::escape_keyword(&node.name)),
491                        generics: Vec::new(),
492                        variants,
493                    }),
494                ));
495                continue;
496            }
497            TypeKind::Bitmask { bits } => {
498                if ghost_created_for(applied, node.node_id, "RustBitmask").is_none() {
499                    continue;
500                }
501                // A bitmask becomes the integer its bit bound calls for,
502                // with one constant per bit: a Rust enum could not hold
503                // two bits at once, which is the point of a bitmask.
504                let width = ast.effective_bit_bound(id);
505                items.push((
506                    node.module,
507                    rust::Item::TypeAlias(rust::TypeAlias {
508                        doc: Doc::none(),
509                        attrs: Vec::new(),
510                        visibility: Visibility::Public,
511                        name: Ident::new(&rust::escape_keyword(&node.name)),
512                        generics: Vec::new(),
513                        type_: rust::Type::Primitive(bitmask_primitive(width)),
514                    }),
515                ));
516                for bit in bits {
517                    let Ok(bit_node) = ast.bit_value(*bit) else {
518                        continue;
519                    };
520                    if ghost_created_for(applied, bit_node.node_id, "RustBitConst").is_none() {
521                        continue;
522                    }
523                    let position = ast.effective_bit_position(*bit).ok_or_else(|| {
524                        MaterialiseError::Unevaluated {
525                            construct: "bit position".to_string(),
526                            at: bit_node.name.clone(),
527                        }
528                    })?;
529                    items.push((
530                        node.module,
531                        rust::Item::Const(rust::Const {
532                            doc: Doc::none(),
533                            attrs: Vec::new(),
534                            visibility: Visibility::Public,
535                            name: Ident::new(&rust::escape_keyword(&bit_node.name)),
536                            type_: rust::Type::Primitive(bitmask_primitive(width)),
537                            value: rust::Expr::Binary {
538                                op: rust::BinaryOp::Shl,
539                                lhs: Box::new(rust::Expr::Literal(
540                                    zerodds_lang_cst::common::Literal::int(1),
541                                )),
542                                rhs: Box::new(rust::Expr::Literal(
543                                    zerodds_lang_cst::common::Literal::int(i128::from(position)),
544                                )),
545                            },
546                        }),
547                    ));
548                }
549                continue;
550            }
551            TypeKind::Bitset { .. } => {
552                if ghost_created_for(applied, node.node_id, "RustBitset").is_none() {
553                    continue;
554                }
555                // Each bitfield becomes a field of the holder type its
556                // width calls for, the base's included; the packing is
557                // the layout's business and not the source's shape.
558                let mut struct_fields = Vec::new();
559                for field in &ast.all_bitfields_of(id) {
560                    let Ok(field_node) = ast.bitfield(*field) else {
561                        continue;
562                    };
563                    // An anonymous bitfield is padding, and padding has
564                    // no name to declare.
565                    let Some(name) = field_node.name.as_ref() else {
566                        continue;
567                    };
568                    struct_fields.push(rust::Field {
569                        doc: Doc::none(),
570                        attrs: Vec::new(),
571                        visibility: Visibility::Public,
572                        name: Ident::new(&rust::escape_keyword(name)),
573                        type_: rust::Type::Primitive(bitmask_primitive(field_node.bits)),
574                    });
575                }
576                items.push((
577                    node.module,
578                    rust::Item::Struct(rust::Struct {
579                        body_text: zerodds_lang_cst::common::BodyText::none(),
580                        doc: Doc::none(),
581                        attrs: Vec::new(),
582                        // `Debug`, `Clone` and `PartialEq` and nothing
583                        // else. The rule this file states — a derive is a
584                        // decision about the target language and not about
585                        // the wire — is about `Default`, whose Rust answer
586                        // and Table 9's part company at an enumeration
587                        // whose first literal is not zero. These three make
588                        // no claim about the wire at all, and without them
589                        // a generated type cannot be printed in an
590                        // assertion, compared with another, or held twice.
591                        // `crates/corba-interop`'s harness needs all three,
592                        // and `Eq` is not among them because a `double`
593                        // member rules it out.
594                        derives: data_derives(),
595                        visibility: Visibility::Public,
596                        name: Ident::new(&rust::escape_keyword(&node.name)),
597                        generics: Vec::new(),
598                        body: rust::StructBody::Named(struct_fields),
599                    }),
600                ));
601                continue;
602            }
603            TypeKind::Valuetype { state, .. } => {
604                if ghost_created_for(applied, node.node_id, "RustValueStruct").is_none() {
605                    continue;
606                }
607                let mut struct_fields = Vec::new();
608                for member in state {
609                    let Ok(member_node) = ast.member(*member) else {
610                        continue;
611                    };
612                    // A valuetype's state members are `Ast::members_of`
613                    // just as a structure's are, so the planner frames an
614                    // `@optional` one the same way and the field has to
615                    // hold the same absence.
616                    struct_fields.push(rust::Field {
617                        doc: Doc::none(),
618                        attrs: Vec::new(),
619                        visibility: Visibility::Public,
620                        name: Ident::new(&rust::escape_keyword(&member_node.name)),
621                        type_: field_type(ast, from, member_node)?,
622                    });
623                }
624                items.push((
625                    node.module,
626                    rust::Item::Struct(rust::Struct {
627                        body_text: zerodds_lang_cst::common::BodyText::none(),
628                        doc: Doc::none(),
629                        attrs: Vec::new(),
630                        // `Debug`, `Clone` and `PartialEq` and nothing
631                        // else. The rule this file states — a derive is a
632                        // decision about the target language and not about
633                        // the wire — is about `Default`, whose Rust answer
634                        // and Table 9's part company at an enumeration
635                        // whose first literal is not zero. These three make
636                        // no claim about the wire at all, and without them
637                        // a generated type cannot be printed in an
638                        // assertion, compared with another, or held twice.
639                        // `crates/corba-interop`'s harness needs all three,
640                        // and `Eq` is not among them because a `double`
641                        // member rules it out.
642                        derives: data_derives(),
643                        visibility: Visibility::Public,
644                        name: Ident::new(&rust::escape_keyword(&node.name)),
645                        generics: Vec::new(),
646                        body: rust::StructBody::Named(struct_fields),
647                    }),
648                ));
649                continue;
650            }
651            TypeKind::Native => {
652                if ghost_created_for(applied, node.node_id, "RustNativeAlias").is_none() {
653                    continue;
654                }
655                // A native type is opaque: the alias names it and the
656                // mapping to a concrete type comes from the facet, not
657                // from a guess made here.
658                items.push((
659                    node.module,
660                    rust::Item::TypeAlias(rust::TypeAlias {
661                        doc: Doc::none(),
662                        attrs: Vec::new(),
663                        visibility: Visibility::Public,
664                        name: Ident::new(&rust::escape_keyword(&node.name)),
665                        generics: Vec::new(),
666                        type_: native_type(ast, id),
667                    }),
668                ));
669                continue;
670            }
671            TypeKind::Structure { .. } => {}
672            _ => continue,
673        }
674        if ghost_created_for(applied, node.node_id, "RustStruct").is_none() {
675            continue;
676        }
677
678        // XTypes 1.3 §7.5.1.4.1.3: a language with no mapping of its own for
679        // inheritance binds the derived structure "as if an instance of the base type
680        // were the first member of the subtype with the name 'parent'". The wire
681        // still carries the base's members first (§7.4.1.1.5.1): the codec reaches
682        // them through `parent`.
683        let mut fields = Vec::new();
684        let mut spelled = Vec::new();
685        if let Some(base) = crate::adapter::c::struct_base(ast, id) {
686            spelled.push((PARENT.to_string(), PARENT.to_string()));
687            fields.push(rust::Field {
688                doc: Doc::line("The base structure, the first member (XTypes 1.3 §7.5.1.4.1.3)."),
689                attrs: Vec::new(),
690                visibility: Visibility::Public,
691                name: Ident::new(PARENT),
692                type_: rust_type_of(ast, from, Some(base))?,
693            });
694        }
695        for member in ast.members_of(id) {
696            let Ok(member_node) = ast.member(*member) else {
697                continue;
698            };
699            if ghost_created_for(applied, member_node.node_id, "RustField").is_none() {
700                continue;
701            }
702            let field = rust::escape_keyword(&member_node.name);
703            spelled.push((field.clone(), member_node.name.clone()));
704            fields.push(rust::Field {
705                doc: Doc::none(),
706                attrs: Vec::new(),
707                visibility: Visibility::Public,
708                name: Ident::new(&field),
709                type_: field_type(ast, from, member_node)?,
710            });
711        }
712        no_spelling_collision(
713            spelled
714                .iter()
715                .map(|(field, declared)| (field.as_str(), declared.as_str())),
716            "Rust",
717            &node.name,
718        )?;
719
720        items.push((
721            node.module,
722            rust::Item::Struct(rust::Struct {
723                body_text: zerodds_lang_cst::common::BodyText::none(),
724                doc: Doc::none(),
725                attrs: Vec::new(),
726                // `Debug`, `Clone` and `PartialEq` and nothing
727                // else. The rule this file states — a derive is a
728                // decision about the target language and not about
729                // the wire — is about `Default`, whose Rust answer
730                // and Table 9's part company at an enumeration
731                // whose first literal is not zero. These three make
732                // no claim about the wire at all, and without them
733                // a generated type cannot be printed in an
734                // assertion, compared with another, or held twice.
735                // `crates/corba-interop`'s harness needs all three,
736                // and `Eq` is not among them because a `double`
737                // member rules it out.
738                derives: data_derives(),
739                visibility: Visibility::Public,
740                name: Ident::new(&rust::escape_keyword(&node.name)),
741                generics: Vec::new(),
742                body: rust::StructBody::Named(fields),
743            }),
744        ));
745    }
746
747    weave_rust_verbatim(ast, &starts, &mut items);
748
749    // Constants, exceptions and interfaces are not types, so they are
750    // walked separately — the model keeps them in arenas of their own.
751    for (id, node) in ast.constants() {
752        // Every type this item names is written relative to the module the
753        // item itself lands in.
754        let from = node.module;
755        if ghost_created_for(applied, node.node_id, "RustConst").is_none() {
756            continue;
757        }
758        // `eval_const` and not `eval_integral`: `const string NAME`
759        // evaluates perfectly well and is not a number, and asking the
760        // integral question about it answered `Unevaluated` and dropped
761        // the item.
762        let value = ast
763            .eval_const(node.expr)
764            .map_err(|_| MaterialiseError::Unevaluated {
765                construct: "constant's value".to_string(),
766                at: node.name.clone(),
767            })?;
768        let declared = rust_type_of(ast, from, node.type_.resolved())?;
769        let (type_, literal) = rust_constant(&value, declared, &node.name)?;
770        items.push((
771            node.module,
772            rust::Item::Const(rust::Const {
773                doc: Doc::none(),
774                attrs: Vec::new(),
775                visibility: Visibility::Public,
776                name: Ident::new(&rust::escape_keyword(&node.name)),
777                type_,
778                value: rust::Expr::Literal(literal),
779            }),
780        ));
781        let _ = id;
782    }
783
784    for (id, node) in ast.exceptions() {
785        // Every type this item names is written relative to the module the
786        // item itself lands in.
787        let from = node.module;
788        if ghost_created_for(applied, node.node_id, "RustErrorStruct").is_none() {
789            continue;
790        }
791        let mut fields = Vec::new();
792        for member in &node.members {
793            let Ok(member_node) = ast.member(*member) else {
794                continue;
795            };
796            fields.push(rust::Field {
797                doc: Doc::line(&format!("The IDL exception member `{}`.", member_node.name)),
798                attrs: Vec::new(),
799                visibility: Visibility::Public,
800                name: Ident::new(&rust::escape_keyword(&member_node.name)),
801                type_: rust_type_of(ast, from, member_node.type_.resolved())?,
802            });
803        }
804        items.push((
805            node.module,
806            rust::Item::Struct(rust::Struct {
807                body_text: zerodds_lang_cst::common::BodyText::none(),
808                doc: Doc::line(&format!(
809                    "The IDL exception `{}`, as `Err` payload — DDS-RPC 1.0 \
810                     §7.5.1.1.5 gives each raised exception its own case in \
811                     the operation's `_Result` union.",
812                    node.name
813                )),
814                attrs: Vec::new(),
815                // `Debug`, `Clone` and `PartialEq` and nothing
816                // else. The rule this file states — a derive is a
817                // decision about the target language and not about
818                // the wire — is about `Default`, whose Rust answer
819                // and Table 9's part company at an enumeration
820                // whose first literal is not zero. These three make
821                // no claim about the wire at all, and without them
822                // a generated type cannot be printed in an
823                // assertion, compared with another, or held twice.
824                // `crates/corba-interop`'s harness needs all three,
825                // and `Eq` is not among them because a `double`
826                // member rules it out.
827                derives: data_derives(),
828                visibility: Visibility::Public,
829                name: Ident::new(&rust::escape_keyword(&node.name)),
830                generics: Vec::new(),
831                body: rust::StructBody::Named(fields),
832            }),
833        ));
834        let _ = id;
835    }
836
837    for (id, node) in ast.interfaces() {
838        // Every type this item names is written relative to the module the
839        // item itself lands in.
840        let from = node.module;
841        if ghost_created_for(applied, node.node_id, "RustTrait").is_none() {
842            continue;
843        }
844        // An interface becomes a trait of signatures. The transport is
845        // the RPC arc's business; what belongs here is the shape the
846        // IDL declared.
847        //
848        // The whole answer and not the declared one: `supertraits` below
849        // is empty, so a derived interface's trait has no supertrait to
850        // reach its bases' operations through and OMG IDL 4.2 §7.4.3's
851        // inherited operations are written into it. See
852        // [`Ast::all_operations_of`].
853        let mut methods = Vec::new();
854        for operation in &ast.all_operations_of(id) {
855            let Ok(op) = ast.operation(*operation) else {
856                continue;
857            };
858            let mut params = vec![rust::Param {
859                name: None,
860                type_: rust::Type::unit(),
861                receiver: Some(rust::Receiver::Ref),
862            }];
863            for param in &op.params {
864                let Ok(param_node) = ast.param(*param) else {
865                    continue;
866                };
867                params.push(rust::Param {
868                    name: Some(Ident::new(&rust::escape_keyword(&param_node.name))),
869                    type_: rust_type_of(ast, from, param_node.type_.resolved())?,
870                    receiver: None,
871                });
872            }
873            methods.push(rust::Item::Function(rust::Function {
874                doc: Doc::none(),
875                attrs: Vec::new(),
876                visibility: Visibility::Private,
877                name: Ident::new(&rust::escape_keyword(&op.name)),
878                generics: Vec::new(),
879                params,
880                returns: op
881                    .return_type
882                    .as_ref()
883                    .map(|type_| rust_type_of(ast, from, type_.resolved()))
884                    .transpose()?,
885                body: None,
886            }));
887        }
888        items.push((
889            node.module,
890            rust::Item::Trait(rust::Trait {
891                doc: Doc::none(),
892                attrs: Vec::new(),
893                visibility: Visibility::Public,
894                name: Ident::new(&rust::escape_keyword(&node.name)),
895                generics: Vec::new(),
896                supertraits: Vec::new(),
897                items: methods,
898            }),
899        ));
900    }
901
902    Ok(items)
903}
904
905/// The declarations, each inside the `pub mod` its IDL module names.
906///
907/// Public because the codecs are a second call —
908/// `crate::encode::rust_codec`, once per type — and they have to land
909/// in the same module as the declaration they are an `impl` of. A
910/// caller that emitted both and nested only the first would produce an
911/// `impl` for a name that is not in scope.
912///
913/// The root module is the anonymous global scope and produces no
914/// `mod`; every other one nests, outermost first, and a module with no
915/// materialised declaration produces nothing — an empty `pub mod` is a
916/// name in the output that nothing put there.
917///
918/// Declaration order is preserved within a module, and the modules
919/// follow the order their first declaration appeared in, so the output
920/// tracks the source rather than a hash.
921///
922/// # Errors
923/// [`MaterialiseError::NoModule`] when the model does not answer for a
924/// module an item names. Returning the item unwrapped instead put it in
925/// the enclosing scope and said nothing.
926pub fn nest_by_module(
927    ast: &Ast,
928    items: Vec<(Id<zerodds_ast::ModuleNode>, rust::Item)>,
929) -> Result<Vec<rust::Item>, MaterialiseError> {
930    // Each item is placed by its **whole** module path and not by its
931    // innermost module. Grouping by the innermost one and wrapping each
932    // group in its own chain gives two declarations of every shared
933    // ancestor: `spatial::core::A` and `spatial::sensing::B` became
934    // `mod spatial { mod core { A } }` beside `mod spatial { mod sensing
935    // { B } }`, and Rust — unlike IDL, where §7.5 lets a module be
936    // reopened — allows a name once per scope. The generated file did
937    // not compile: `the name `spatial` is defined multiple times`.
938    let mut entries: Vec<PathedItem> = Vec::with_capacity(items.len());
939    for (module, item) in items {
940        entries.push((module_path(ast, module)?, item));
941    }
942    fold_by_path(ast, entries, 0)
943}
944
945/// A declared type's path as written from inside module `from`.
946///
947/// The simple name alone only resolves inside the type's own module. A
948/// member of `spatial::sensing::FrameRef` whose type is
949/// `spatial::core::CoordConvention` was emitted as `CoordConvention`,
950/// which does not resolve there: the folded module carries
951/// `use super::*`, and that reaches the enclosing scope and not a
952/// sibling.
953///
954/// The path is **relative** and not rooted at the crate. An absolute
955/// `crate::a::B::C` was tried first and is wrong: it resolves for
956/// `crates/spatial-dds`, whose `src/lib.rs` includes the generated file
957/// at the crate root, and breaks `crates/wire-conformance`, which
958/// includes its own inside a module — `cannot find type `Cell` in the
959/// crate root`. A relative path does not depend on where the file is
960/// included.
961///
962/// Same module means no prefix, which is why every generator that emits
963/// one module's worth of types is unaffected by this.
964///
965/// # Errors
966/// [`MaterialiseError::NoModule`] when the model does not answer for a
967/// module in either chain.
968pub(crate) fn relative_path(
969    ast: &Ast,
970    from: Id<zerodds_ast::ModuleNode>,
971    node: &zerodds_ast::TypeNode,
972) -> Result<Path, MaterialiseError> {
973    let here = module_path(ast, from)?;
974    let there = module_path(ast, node.module)?;
975    let shared = here
976        .iter()
977        .zip(there.iter())
978        .take_while(|(a, b)| a == b)
979        .count();
980
981    let mut segments: Vec<String> = Vec::new();
982    // One `super` per module left behind…
983    for _ in shared..here.len() {
984        segments.push(String::from("super"));
985    }
986    // …then down into the ones the target adds.
987    for module in there.iter().skip(shared) {
988        let enclosing = ast
989            .module(*module)
990            .map_err(|_| MaterialiseError::NoModule {
991                at: module_handle(*module),
992            })?;
993        segments.push(rust::escape_keyword(&enclosing.name));
994    }
995    segments.push(rust::escape_keyword(&node.name));
996    let borrowed: Vec<&str> = segments.iter().map(String::as_str).collect();
997    Ok(Path::new(&borrowed))
998}
999
1000/// A declared type's path as written from the **top level of the
1001/// generated file** — `common::util::Location`.
1002///
1003/// The counterpart of `relative_path` for a caller that is not inside
1004/// one of the folded modules. `nest_by_module` puts every type under one
1005/// `pub mod` per IDL module, so an item appended after that tree — a
1006/// trait impl, say — names a type by the chain of modules it sits in.
1007///
1008/// Relative and not rooted at the crate, for the reason
1009/// `relative_path` gives: the generated file is included at a crate
1010/// root in one consumer and inside a module in another, and only a
1011/// relative path holds in both.
1012///
1013/// # Errors
1014/// [`MaterialiseError::NoModule`] when the model does not answer for a
1015/// module in the chain, or [`MaterialiseError::NoOrigin`] when it does
1016/// not answer for the type.
1017pub fn path_from_file_root(ast: &Ast, type_: Id<TypeNode>) -> Result<String, MaterialiseError> {
1018    let node = ast.type_(type_).map_err(|_| MaterialiseError::NoOrigin {
1019        type_name: format!("{type_:?}"),
1020    })?;
1021    let mut segments: Vec<String> = Vec::new();
1022    for module in module_path(ast, node.module)? {
1023        let enclosing = ast.module(module).map_err(|_| MaterialiseError::NoModule {
1024            at: module_handle(module),
1025        })?;
1026        segments.push(rust::escape_keyword(&enclosing.name));
1027    }
1028    segments.push(rust::escape_keyword(&node.name));
1029    Ok(segments.join("::"))
1030}
1031
1032/// One item with the module path it belongs at, outermost first.
1033type PathedItem = (Vec<Id<zerodds_ast::ModuleNode>>, rust::Item);
1034
1035/// The chain of modules enclosing `module`, outermost first, with the
1036/// unnamed root left out.
1037///
1038/// # Errors
1039/// [`MaterialiseError::NoModule`] when the model does not answer for a
1040/// module in the chain, or when the chain runs past
1041/// [`MAX_MODULE_DEPTH`].
1042pub(crate) fn module_path(
1043    ast: &Ast,
1044    module: Id<zerodds_ast::ModuleNode>,
1045) -> Result<Vec<Id<zerodds_ast::ModuleNode>>, MaterialiseError> {
1046    let mut path = Vec::new();
1047    let mut current = module;
1048    for _ in 0..=MAX_MODULE_DEPTH {
1049        let node = ast
1050            .module(current)
1051            .map_err(|_| MaterialiseError::NoModule {
1052                at: module_handle(current),
1053            })?;
1054        // The root has no name and is not a `mod`.
1055        let Some(parent) = node.parent else {
1056            path.reverse();
1057            return Ok(path);
1058        };
1059        path.push(current);
1060        current = parent;
1061    }
1062    Err(MaterialiseError::NoModule {
1063        at: format!(
1064            "{} — the parent chain runs past {MAX_MODULE_DEPTH}",
1065            module_handle(module)
1066        ),
1067    })
1068}
1069
1070/// `entries`, folded into one `pub mod` per distinct module at `depth`.
1071///
1072/// Order is first appearance, at every level: a module is declared where
1073/// its first item put it, and every later item of the same module joins
1074/// that declaration rather than opening a second one.
1075///
1076/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
1077fn fold_by_path(
1078    ast: &Ast,
1079    entries: Vec<PathedItem>,
1080    depth: usize,
1081) -> Result<Vec<rust::Item>, MaterialiseError> {
1082    if depth > MAX_MODULE_DEPTH {
1083        return Err(MaterialiseError::NoModule {
1084            at: format!("a module chain runs past {MAX_MODULE_DEPTH}"),
1085        });
1086    }
1087    let mut here: Vec<rust::Item> = Vec::new();
1088    let mut order: Vec<Id<zerodds_ast::ModuleNode>> = Vec::new();
1089    let mut deeper: Vec<(Id<zerodds_ast::ModuleNode>, Vec<PathedItem>)> = Vec::new();
1090
1091    for (path, item) in entries {
1092        match path.get(depth).copied() {
1093            None => here.push(item),
1094            Some(module) => match order.iter().position(|id| *id == module) {
1095                Some(at) => deeper[at].1.push((path, item)),
1096                None => {
1097                    order.push(module);
1098                    deeper.push((module, vec![(path, item)]));
1099                }
1100            },
1101        }
1102    }
1103
1104    // Items that stop here keep their place ahead of the modules that
1105    // follow them, which is the order the source declared.
1106    let mut out = here;
1107    for (module, nested) in deeper {
1108        let node = ast.module(module).map_err(|_| MaterialiseError::NoModule {
1109            at: module_handle(module),
1110        })?;
1111        out.push(rust::Item::Module(rust::Module {
1112            // `missing_docs` fires on every `pub mod` a consumer's own
1113            // `-D warnings` turns on, same as it fires on the type this
1114            // module holds; one line here answers it for every column
1115            // that folds through this function.
1116            doc: Doc::line(&format!("The IDL module `{}`.", node.name)),
1117            attrs: Vec::new(),
1118            visibility: Visibility::Public,
1119            name: Ident::new(&rust::escape_keyword(&node.name)),
1120            // A module is a scope: everything the file imported, and every
1121            // type declared beside this module, has to be reachable from
1122            // inside it. `use super::*` is one line per module and reaches
1123            // both, which a list of named imports would not — a codec here
1124            // names types from any module the IDL let it name.
1125            uses: vec![rust::Use {
1126                path: Path::single("super"),
1127                names: vec![Ident::new("*")],
1128                visibility: Visibility::Private,
1129            }],
1130            items: fold_by_path(ast, nested, depth + 1)?,
1131        }));
1132    }
1133    Ok(out)
1134}
1135
1136/// How deep a module chain is followed before the model is taken to be
1137/// malformed. IDL nests nowhere near this.
1138const MAX_MODULE_DEPTH: usize = 64;
1139
1140/// How deep a type's Rust spelling may nest before the walk refuses it.
1141///
1142/// The same 64 every reference walk in the tree carries;
1143/// `zerodds-wire-codec`'s `default.rs` and `zerodds-types`'
1144/// `dynamic/type_.rs` both stop there.
1145const MAX_TYPE_DEPTH: usize = 64;
1146
1147/// The module handle, named without the node the model would not answer
1148/// for. Slot and generation are what the arena was asked for.
1149pub(crate) fn module_handle(module: Id<zerodds_ast::ModuleNode>) -> String {
1150    handle_of(module)
1151}
1152
1153/// A handle, named as far as it can be without the node behind it.
1154///
1155/// Used where the model would not hand a node over: there is no name to
1156/// report, because the name lives in the node. The slot and generation
1157/// are what the arena knows, and they are enough to find the entry that
1158/// went missing.
1159pub(crate) fn handle_of<T>(id: Id<T>) -> String {
1160    format!("#{}@{}", id.slot(), id.generation())
1161}
1162
1163/// The field type one member declaration takes.
1164///
1165/// `Box<T>` for external storage, wrapped in `Option` when also optional.
1166/// Otherwise `Option<T>` for an optional member and `T` for an ordinary one.
1167/// DDS-XTypes 1.3 §7.2.2.4.4.4.7 makes the member's *value* optional and
1168/// not its type: "the member may or may not be present in a given
1169/// object of that type". A field of the member's own type has no way to
1170/// say "not present", so the column that generated one could only refuse
1171/// a stream — in either encoding version — that says the member is
1172/// absent, which is a sample the peers on the other three columns hand
1173/// back without comment.
1174///
1175/// This is a type-mapping decision and not a wire one. The bytes are
1176/// rule (19)'s parameter header of declared length zero and rule (20)'s
1177/// BOOLEAN `<is_present>` of `0`, both of which the encoder already
1178/// wrote; what changes is that the sample can now hold the absence they
1179/// describe.
1180///
1181/// # Errors
1182/// As [`rust_items`].
1183fn field_type(
1184    ast: &Ast,
1185    from: Id<zerodds_ast::ModuleNode>,
1186    member: &zerodds_ast::MemberNode,
1187) -> Result<rust::Type, MaterialiseError> {
1188    let mut inner = rust_type_of(ast, from, member.type_.resolved())?;
1189    if is_external(ast, member) {
1190        inner = rust::Type::generic(Path::single("Box"), vec![inner]);
1191    }
1192    if member.facets.is_optional() {
1193        return Ok(rust::Type::generic(Path::new(&["Option"]), vec![inner]));
1194    }
1195    Ok(inner)
1196}
1197
1198/// How the Rust column holds an IDL `map`.
1199///
1200/// XTypes 1.3 §7.5.1.3.6: "In all languages for which no language-specific mapping is
1201/// specified, the language binding for map types shall be based on the equivalent IDL definition
1202/// given in 7.4.1.1.4", the sequence of `MapEntry` structures; that is the default.
1203/// [`RustMapForm::BTreeMap`] is the idiomatic container and a named option: it has the
1204/// operations of a map and loses the order the entries were put in, which is ascending by key.
1205#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1206pub enum RustMapForm {
1207    /// `Vec<zerodds_wire::MapEntry<K, V>>`.
1208    #[default]
1209    Entries,
1210    /// `BTreeMap<K, V>`.
1211    BTreeMap,
1212}
1213
1214thread_local! {
1215    static MAP_FORM: std::cell::Cell<RustMapForm> = const { std::cell::Cell::new(RustMapForm::Entries) };
1216}
1217
1218/// Selects the form of a Rust map on the current thread until dropped.
1219///
1220/// A scope and not a parameter for the reason [`super::cpp::CppMappingScope`] gives.
1221#[derive(Debug)]
1222pub struct RustMapFormScope {
1223    previous: RustMapForm,
1224}
1225
1226impl RustMapFormScope {
1227    /// Selects `form` on this thread.
1228    #[must_use]
1229    pub fn enter(form: RustMapForm) -> Self {
1230        let previous = MAP_FORM.with(|cell| cell.replace(form));
1231        Self { previous }
1232    }
1233}
1234
1235impl Drop for RustMapFormScope {
1236    fn drop(&mut self) {
1237        MAP_FORM.with(|cell| cell.set(self.previous));
1238    }
1239}
1240
1241/// The form of a Rust map in force on this thread.
1242#[must_use]
1243pub fn rust_map_form() -> RustMapForm {
1244    MAP_FORM.with(std::cell::Cell::get)
1245}
1246
1247/// The name of the member that holds the base of a derived structure (XTypes 1.3 §7.5.1.4.1.3).
1248pub(crate) const PARENT: &str = "parent";
1249
1250/// XTypes' external storage is independent of optional presence. Rust owns
1251/// that storage through a Box; Option, when present, wraps the Box.
1252///
1253/// A member that closes a type cycle is held the same way, annotated or
1254/// not ([`super::held_externally`]): `Option<Node>` inside `Node` is
1255/// `error[E0072]: recursive type has infinite size`, and the Box is the
1256/// indirection rustc's own help text asks for.
1257pub(crate) fn is_external(ast: &Ast, member: &zerodds_ast::MemberNode) -> bool {
1258    super::held_externally(ast, member)
1259}
1260
1261pub(crate) fn external_value(
1262    ast: &Ast,
1263    member: &zerodds_ast::MemberNode,
1264    value: rust::Expr,
1265) -> rust::Expr {
1266    if is_external(ast, member) {
1267        rust::Expr::Call {
1268            callee: Box::new(rust::Expr::Path(Path::new(&["Box", "new"]))),
1269            args: vec![value],
1270        }
1271    } else {
1272        value
1273    }
1274}
1275
1276/// The Rust variant name a union case member takes.
1277///
1278/// Rust variants are upper camel case and IDL members are not; the name
1279/// is the member's with its first letter raised, so the two stay
1280/// recognisably the same name.
1281pub(crate) fn variant_name(member: &str) -> String {
1282    let mut chars = member.chars();
1283    match chars.next() {
1284        None => String::new(),
1285        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1286    }
1287}
1288
1289/// The unsigned integer a bit width calls for.
1290pub(crate) const fn bitmask_primitive(bits: u16) -> rust::Primitive {
1291    match bits {
1292        0..=8 => rust::Primitive::U8,
1293        9..=16 => rust::Primitive::U16,
1294        17..=32 => rust::Primitive::U32,
1295        _ => rust::Primitive::U64,
1296    }
1297}
1298
1299/// What a `native` declaration maps to.
1300///
1301/// The facet if a `#pragma` or an annotation gave one, and an opaque
1302/// pointer otherwise — never a guess at what the name might mean.
1303fn native_type(ast: &Ast, id: zerodds_ast_core::Id<zerodds_ast::TypeNode>) -> rust::Type {
1304    let mapping = ast
1305        .any_of_type(id)
1306        .ok()
1307        .and_then(|any| ast.facets(any))
1308        .and_then(|facets| facets.corba.as_ref())
1309        .and_then(|corba| corba.native_mapping.clone());
1310    match mapping {
1311        Some(name) => rust::Type::named(Path::single(&name)),
1312        None => rust::Type::Ref {
1313            target: Box::new(rust::Type::Primitive(rust::Primitive::U8)),
1314            mutable: false,
1315            lifetime: None,
1316        },
1317    }
1318}
1319
1320/// Whether the rules created a node of `type_name` for `node`.
1321///
1322/// The correspondence the rule established anchors on the model node, so
1323/// the created node's identity is derived from it.
1324fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
1325    let anchor = applied
1326        .origin
1327        .iter()
1328        .find(|(_, model)| **model == node)
1329        .map(|(ghost, _)| *ghost)?;
1330    let typ = applied.graph.types.lookup(type_name)?;
1331    applied
1332        .graph
1333        .nodes_of_type(typ)
1334        .map(|created| created.id)
1335        .find(|created| {
1336            applied.graph.connected(&anchor, created) || is_derived_from(applied, &anchor, created)
1337        })
1338}
1339
1340/// Whether a created node hangs off `anchor` through a correspondence.
1341fn is_derived_from(applied: &Applied, anchor: &GhostId, created: &GhostId) -> bool {
1342    // A correspondence is an ordinary node with two links: one to the
1343    // anchor, one to what was created. Two hops rather than one.
1344    applied
1345        .graph
1346        .parts(anchor)
1347        .any(|part| applied.graph.connected(&part.other, created))
1348}
1349
1350/// The Rust type a model type becomes.
1351///
1352/// Total over `TypeKind` with no catch-all: a kind added to the model
1353/// without a mapping here is a compile error rather than a field that
1354/// silently takes some other type.
1355///
1356/// `pub(crate)`: `emit::interface_rust` spells an operation parameter's
1357/// or an attribute's type the same way a member's is spelled — the two
1358/// are one clause apart (OMG IDL 4.2 §7.4.3) and a second reading would
1359/// disagree about, say, whether `long double` takes `zerodds_float128`.
1360pub(crate) fn rust_type_of(
1361    ast: &Ast,
1362    from: Id<zerodds_ast::ModuleNode>,
1363    target: Option<zerodds_ast_core::Id<zerodds_ast::TypeNode>>,
1364) -> Result<rust::Type, MaterialiseError> {
1365    type_of_at(ast, from, target, 0)
1366}
1367
1368/// [`rust_type_of`], carrying how many connector steps the walk has
1369/// already taken.
1370///
1371/// The `Alias` arm follows every `typedef`, named ones included, so a
1372/// `typedef` cycle — a finding the model's invariant pass reports rather
1373/// than a shape it rejects — used to read forever here.
1374///
1375/// # Errors
1376/// [`MaterialiseError::Unevaluated`] when the spelling nests past
1377/// `MAX_TYPE_DEPTH`.
1378///
1379/// zerodds-lint: recursion-depth 64 (sequence, array, map, value-box and
1380/// typedef nesting; bounded by `MAX_TYPE_DEPTH`, which returns
1381/// `MaterialiseError::Unevaluated`)
1382fn type_of_at(
1383    ast: &Ast,
1384    from: Id<zerodds_ast::ModuleNode>,
1385    target: Option<zerodds_ast_core::Id<zerodds_ast::TypeNode>>,
1386    depth: usize,
1387) -> Result<rust::Type, MaterialiseError> {
1388    if depth > MAX_TYPE_DEPTH {
1389        return Err(MaterialiseError::Unevaluated {
1390            construct: format!("type, whose Rust spelling nests past {MAX_TYPE_DEPTH}"),
1391            at: target.map_or_else(|| "a member".to_string(), crate::adapter::cst::handle_of),
1392        });
1393    }
1394    let Some(target) = target else {
1395        // An unresolved member type is a frontend failure the model
1396        // already reports; the field takes the unit type so the shape of
1397        // the output still shows what is missing.
1398        return Ok(rust::Type::unit());
1399    };
1400    let Ok(node) = ast.type_(target) else {
1401        return Ok(rust::Type::unit());
1402    };
1403    Ok(match &node.kind {
1404        // `long double` is the one primitive with no Rust built-in.
1405        // `zerodds_float128::Float128` carries it exactly — the sixteen
1406        // bytes CDR gives it, with the conversions to and from `f64`
1407        // reporting the values that do not fit. It used to take `f64`,
1408        // which put a value nobody wrote on the wire.
1409        TypeKind::Primitive(PrimitiveKind::Float128) => {
1410            rust::Type::named(Path::new(&["zerodds_float128", "Float128"]))
1411        }
1412        TypeKind::Primitive(kind) => rust::Type::Primitive(rust_primitive(*kind)),
1413        TypeKind::String { .. } => rust::Type::Primitive(rust::Primitive::String),
1414        TypeKind::Sequence { element, .. } => rust::Type::generic(
1415            Path::new(&["Vec"]),
1416            vec![type_of_at(ast, from, element.resolved(), depth + 1)?],
1417        ),
1418        TypeKind::Array { element, dims } => {
1419            let mut out = type_of_at(ast, from, element.resolved(), depth + 1)?;
1420            // Innermost dimension first, so `long a[2][3]` becomes
1421            // `[[i32; 3]; 2]` — the outer dimension outermost.
1422            for dim in dims.iter().rev() {
1423                let length =
1424                    ast.eval_integral(*dim)
1425                        .map_err(|_| MaterialiseError::Unevaluated {
1426                            construct: "array length".to_string(),
1427                            at: named(&node.name, node.node_id),
1428                        })?;
1429                out = rust::Type::Array {
1430                    element: Box::new(out),
1431                    length: Box::new(rust::Expr::Literal(zerodds_lang_cst::common::Literal::int(
1432                        length,
1433                    ))),
1434                };
1435            }
1436            out
1437        }
1438        TypeKind::Map { key, value, .. } => {
1439            let key = type_of_at(ast, from, key.resolved(), depth + 1)?;
1440            let value = type_of_at(ast, from, value.resolved(), depth + 1)?;
1441            match rust_map_form() {
1442                RustMapForm::BTreeMap => {
1443                    rust::Type::generic(Path::new(&["BTreeMap"]), vec![key, value])
1444                }
1445                // XTypes 1.3 §7.5.1.3.6: the sequence of `MapEntry`.
1446                RustMapForm::Entries => rust::Type::generic(
1447                    Path::new(&["Vec"]),
1448                    vec![rust::Type::generic(
1449                        Path::new(&["zerodds_wire", "MapEntry"]),
1450                        vec![key, value],
1451                    )],
1452                ),
1453            }
1454        }
1455        TypeKind::Alias { aliased } => type_of_at(ast, from, aliased.resolved(), depth + 1)?,
1456        TypeKind::Structure { .. }
1457        | TypeKind::Union { .. }
1458        | TypeKind::Enumeration { .. }
1459        | TypeKind::Bitmask { .. }
1460        | TypeKind::Bitset { .. }
1461        | TypeKind::Valuetype { .. }
1462        | TypeKind::Native
1463        | TypeKind::AnnotationType { .. } => rust::Type::named(relative_path(ast, from, node)?),
1464        TypeKind::ValueBox { boxed } => rust::Type::generic(
1465            Path::new(&["Box"]),
1466            vec![type_of_at(ast, from, boxed.resolved(), depth + 1)?],
1467        ),
1468        // `fixed<P,S>` becomes the decimal's text, which is what every
1469        // other column does with it — C++ `std::string`, F# and OCaml and
1470        // D their string type — and what the default value
1471        // `zero_decimal(scale)` already spells. It used to emit the name
1472        // `Fixed`, which no crate in the tree declares: ten corpus cases
1473        // rendered, reported success, and failed at `rustc` with
1474        // `cannot find type Fixed in this scope`.
1475        TypeKind::Fixed { .. } => rust::Type::Primitive(rust::Primitive::String),
1476        // The three below have no Rust spelling. Naming them anyway put a
1477        // type nobody declares into a file that reports success — the
1478        // failure this error exists for. C++ refuses them the same way.
1479        TypeKind::ObjectRef { .. } => {
1480            return Err(MaterialiseError::NoLanguageForm {
1481                construct: "Object".to_string(),
1482                language: "Rust",
1483                at: handle_of(target),
1484            });
1485        }
1486        TypeKind::Any => {
1487            return Err(MaterialiseError::NoLanguageForm {
1488                construct: "any".to_string(),
1489                language: "Rust",
1490                at: handle_of(target),
1491            });
1492        }
1493        TypeKind::ValueBase => {
1494            return Err(MaterialiseError::NoLanguageForm {
1495                construct: "ValueBase".to_string(),
1496                language: "Rust",
1497                at: handle_of(target),
1498            });
1499        }
1500    })
1501}
1502
1503/// The derives every materialised data type carries.
1504///
1505/// `pub(crate)`: `emit::interface_rust` gives its own synthesized
1506/// structures and unions the same three, for the same reason this file
1507/// gives them to every declared one.
1508pub(crate) fn data_derives() -> Vec<rust::Derive> {
1509    ["Debug", "Clone", "PartialEq"]
1510        .into_iter()
1511        .map(|name| rust::Derive {
1512            path: Path::single(name),
1513        })
1514        .collect()
1515}
1516
1517/// A model node's name, or its identity when the node is anonymous.
1518///
1519/// An array type declared inline has no name of its own, and an error
1520/// that said only "the array length does not evaluate" would leave the
1521/// reader to find which array.
1522fn named(name: &str, node: zerodds_ast_core::NodeId) -> String {
1523    if name.is_empty() {
1524        format!("{node:?}")
1525    } else {
1526        name.to_string()
1527    }
1528}
1529
1530/// The Rust primitive an IDL primitive becomes.
1531///
1532/// `long double` has no Rust *built-in* and is not routed here: it takes
1533/// `zerodds_float128::Float128`, which is a real binary128. The arm below
1534/// exists so the match is total and answers for a caller that wants the
1535/// nearest built-in; nothing in the materialiser reaches it for a
1536/// `long double`.
1537const fn rust_primitive(kind: PrimitiveKind) -> rust::Primitive {
1538    match kind {
1539        PrimitiveKind::Boolean => rust::Primitive::Bool,
1540        PrimitiveKind::Char => rust::Primitive::Char,
1541        PrimitiveKind::WChar => rust::Primitive::Char,
1542        PrimitiveKind::Octet | PrimitiveKind::UInt8 => rust::Primitive::U8,
1543        PrimitiveKind::Int8 => rust::Primitive::I8,
1544        PrimitiveKind::Int16 => rust::Primitive::I16,
1545        PrimitiveKind::UInt16 => rust::Primitive::U16,
1546        PrimitiveKind::Int32 => rust::Primitive::I32,
1547        PrimitiveKind::UInt32 => rust::Primitive::U32,
1548        PrimitiveKind::Int64 => rust::Primitive::I64,
1549        PrimitiveKind::UInt64 => rust::Primitive::U64,
1550        PrimitiveKind::Float32 => rust::Primitive::F32,
1551        PrimitiveKind::Float64 | PrimitiveKind::Float128 => rust::Primitive::F64,
1552    }
1553}
1554
1555/// The type and the literal a Rust constant takes.
1556///
1557/// The type is not always the member type. `rust_type_of` answers
1558/// `String` for a `string`, a `wstring` and a `fixed`, and a `String` is
1559/// heap-allocated — `const NAME: String` does not compile, because a
1560/// `const` is an expression evaluated at compile time and `String::from`
1561/// is not one. The three become `&'static str`, which is the same bytes
1562/// with no allocation and converts at every use.
1563///
1564/// `long double` is refused by name. `zerodds_float128::Float128` is a
1565/// struct with no literal, and an `f64` would put a different number in
1566/// the output than the IDL wrote — which is the defect the member type's
1567/// own comment records.
1568/// `&'static str`, the type the three string-shaped constants take.
1569fn static_str() -> rust::Type {
1570    rust::Type::Ref {
1571        target: Box::new(rust::Type::Primitive(rust::Primitive::Str)),
1572        mutable: false,
1573        lifetime: Some(Ident::new("static")),
1574    }
1575}
1576
1577fn rust_constant(
1578    value: &zerodds_ast::ConstValue,
1579    declared: rust::Type,
1580    at: &str,
1581) -> Result<(rust::Type, zerodds_lang_cst::common::Literal), MaterialiseError> {
1582    use zerodds_ast::ConstValue;
1583    use zerodds_lang_cst::common::Literal;
1584    let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
1585        construct: construct.to_string(),
1586        language: "Rust",
1587        at: at.to_string(),
1588    };
1589    let is_float128 = matches!(&declared, rust::Type::Named { path, .. } if path
1590        .segments()
1591        .last()
1592        .is_some_and(|last| last.as_str() == "Float128"));
1593    Ok(match value {
1594        ConstValue::Boolean(flag) => (declared, Literal::Bool(*flag)),
1595        ConstValue::Integer(number) => (declared, Literal::int(*number)),
1596        // A Rust `char` is a Unicode scalar value, which is exactly what
1597        // both character forms carry — no width question and no cast.
1598        ConstValue::Char(character) => (declared, Literal::Char(*character)),
1599        ConstValue::WChar(point) => match constant::wchar_char(*point) {
1600            Some(character) => (declared, Literal::Char(character)),
1601            None => {
1602                return Err(refuse(
1603                    "a `wchar` constant that is not a Unicode scalar value",
1604                ));
1605            }
1606        },
1607        ConstValue::Float { mantissa, exponent } => {
1608            if is_float128 {
1609                return Err(refuse(
1610                    "a `long double` constant, which has no Rust literal",
1611                ));
1612            }
1613            // No suffix: the constant's declared type says `f32` or
1614            // `f64`, and a suffix that disagreed with it would be the
1615            // error rather than the fix.
1616            (
1617                declared,
1618                Literal::Float(constant::float_text(mantissa, exponent.as_deref())),
1619            )
1620        }
1621        ConstValue::Fixed { digits, scale } => (
1622            static_str(),
1623            Literal::Str(constant::fixed_decimal(digits, *scale)),
1624        ),
1625        ConstValue::String(text) => (static_str(), Literal::Str(text.clone())),
1626        ConstValue::WString(points) => (
1627            static_str(),
1628            Literal::Str(
1629                constant::wstring_utf8(points)
1630                    .ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
1631            ),
1632        ),
1633        // `ConstValue` is `#[non_exhaustive]`, so a form added to the
1634        // model reaches here. Refused by name rather than given a
1635        // plausible default.
1636        _ => return Err(refuse("a constant form this binding has no literal for")),
1637    })
1638}