Skip to main content

zerodds_lang_rules/adapter/
cpp_psm.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The DDS-PSM-Cxx 1.0 §8.1 mapping of topic types, as a layer over the native one.
4//!
5//! [`CppMapping::Psm`](super::cpp::CppMapping::Psm) keeps the wire codec, the
6//! `TypeObject` and the endpoint gates where they are and gives the application the types
7//! the PSM names:
8//!
9//! | IDL | native (`zd_native::…`) | PSM (the IDL's own namespaces) |
10//! |---|---|---|
11//! | `struct` | struct with public fields | class: private state, accessors of §7.4.6, all-members constructor, `swap` (§7.12) |
12//! | `wchar` | `std::uint16_t` | `wchar_t` |
13//! | `wstring` | `std::string` (UTF-8) | `std::wstring` |
14//! | `long double` | `zerodds::wire::Float128` | `long double` |
15//! | `T a[2][3]` | `std::array<std::array<T, 3>, 2>` | `dds::core::array<dds::core::array<T, 3>, 2>` |
16//! | `map<K,V>` | `std::vector<std::pair<K, V>>` | `std::map<K, V>` |
17//! | `@optional T m` | `T *`, NULL when absent | `dds::core::optional<T>` |
18//! | `@external T m` | `std::shared_ptr<T>` | `dds::core::external<T>` (XTypes 1.3 §7.5.1.2.3.3) |
19//! | `union` | `_d` and one field per case | class: `_d()`, one accessor per case |
20//! | `enum`, `bitmask`, `bitset` | the native type | a `using` of the native type, which already has the IDL's name and constants (§8.1.3) |
21//!
22//! The classes carry no codec. `zerodds::psm::Conv<P>` (`zerodds/psm/convert.hpp`) names the
23//! native type of each PSM class and converts both ways; the `topic_type_support` of a PSM
24//! class calls the native one's `encode` and `decode` through it. So a PSM sample takes the
25//! same bytes, the same `TypeObject` and the same type name as the native one, and costs one
26//! conversion in each direction.
27//!
28//! Not remapped, and left to the native types by name: exceptions, value types, constants.
29//! `@Optional` and `@Shared` in the spelling of the PSM text are not recognised: IDL 4.2
30//! §8.2 makes annotation names case-sensitive, and the wire carries `@optional`.
31
32use super::cpp::{NATIVE_NAMESPACE, cpp_type_path, cxx_name, module_names, without_native_prefix};
33use crate::adapter::c::ordered_types;
34use crate::adapter::cst::MaterialiseError;
35use zerodds_ast::{Ast, CaseLabel, CharWidth, MemberNode, PrimitiveKind, TypeKind, TypeNode};
36use zerodds_ast_core::{Id, Ref};
37
38/// `::a::b::Foo` — the C++ name of the declared type `id` in the PSM namespaces.
39pub fn psm_path(ast: &Ast, id: Id<TypeNode>) -> String {
40    render_path(&without_native_prefix(|| cpp_type_path(ast, id)))
41}
42
43/// `::zd_native::a::b::Foo` — the native type behind it.
44pub fn native_path(ast: &Ast, id: Id<TypeNode>) -> String {
45    let path = without_native_prefix(|| cpp_type_path(ast, id));
46    let rendered = render_path(&path);
47    format!("::{NATIVE_NAMESPACE}{rendered}")
48}
49
50fn render_path(path: &zerodds_lang_cst::common::Path) -> String {
51    let segments: Vec<&str> = path
52        .segments()
53        .iter()
54        .map(zerodds_lang_cst::Ident::as_str)
55        .collect();
56    format!("::{}", segments.join("::"))
57}
58
59fn no_form(construct: &str, at: &str) -> MaterialiseError {
60    MaterialiseError::NoLanguageForm {
61        construct: construct.to_string(),
62        language: "C++ (PSM mapping)",
63        at: at.to_string(),
64    }
65}
66
67/// The PSM spelling of the type `id` (Table 7.1).
68///
69/// # Errors
70/// A construct the PSM mapping has no spelling for, an array length that does not evaluate,
71/// or a type nested too deep.
72pub fn psm_type(ast: &Ast, id: Id<TypeNode>, at: &str) -> Result<String, MaterialiseError> {
73    psm_type_at(ast, id, at, 0)
74}
75
76const MAX_DEPTH: usize = 64;
77
78/// zerodds-lint: recursion-depth 64 (element and key/value types; bounded by `MAX_DEPTH`)
79fn psm_type_at(
80    ast: &Ast,
81    id: Id<TypeNode>,
82    at: &str,
83    depth: usize,
84) -> Result<String, MaterialiseError> {
85    if depth > MAX_DEPTH {
86        return Err(MaterialiseError::Unevaluated {
87            construct: format!("type spelling — it nests past {MAX_DEPTH}"),
88            at: at.to_string(),
89        });
90    }
91    let Ok(node) = ast.type_(id) else {
92        return Err(no_form("a type the model would not hand over", at));
93    };
94    let element = |r: &Ref<TypeNode>| -> Result<String, MaterialiseError> {
95        let Some(target) = r.resolved() else {
96            return Err(no_form("an unresolved element type", at));
97        };
98        psm_type_at(ast, target, at, depth + 1)
99    };
100    Ok(match &node.kind {
101        TypeKind::Primitive(kind) => primitive(*kind).to_string(),
102        TypeKind::String {
103            width: CharWidth::WChar,
104            ..
105        } => "std::wstring".to_string(),
106        TypeKind::String { .. } | TypeKind::Fixed { .. } => "std::string".to_string(),
107        TypeKind::Sequence { element: e, .. } => format!("std::vector<{}>", element(e)?),
108        TypeKind::Array { element: e, dims } => {
109            let mut out = element(e)?;
110            for dim in dims.iter().rev() {
111                let length =
112                    ast.eval_integral(*dim)
113                        .map_err(|_| MaterialiseError::Unevaluated {
114                            construct: "array length".to_string(),
115                            at: at.to_string(),
116                        })?;
117                out = format!("::dds::core::array<{out}, {length}>");
118            }
119            out
120        }
121        TypeKind::Map { key, value, .. } => {
122            format!("std::map<{}, {}>", element(key)?, element(value)?)
123        }
124        TypeKind::Alias { aliased } if node.name.is_empty() => element(aliased)?,
125        TypeKind::Structure { .. }
126        | TypeKind::Union { .. }
127        | TypeKind::Enumeration { .. }
128        | TypeKind::Bitmask { .. }
129        | TypeKind::Bitset { .. }
130        | TypeKind::Alias { .. }
131        | TypeKind::Native
132        | TypeKind::ValueBox { .. } => psm_path(ast, id),
133        _ => return Err(no_form("this construct", at)),
134    })
135}
136
137/// Table 7.1, the primitives. The names are the global ones `<stdint.h>` declares.
138const fn primitive(kind: PrimitiveKind) -> &'static str {
139    match kind {
140        PrimitiveKind::Boolean => "bool",
141        PrimitiveKind::Char => "char",
142        PrimitiveKind::WChar => "wchar_t",
143        PrimitiveKind::Octet | PrimitiveKind::UInt8 => "uint8_t",
144        PrimitiveKind::Int8 => "int8_t",
145        PrimitiveKind::Int16 => "int16_t",
146        PrimitiveKind::UInt16 => "uint16_t",
147        PrimitiveKind::Int32 => "int32_t",
148        PrimitiveKind::UInt32 => "uint32_t",
149        PrimitiveKind::Int64 => "int64_t",
150        PrimitiveKind::UInt64 => "uint64_t",
151        PrimitiveKind::Float32 => "float",
152        PrimitiveKind::Float64 => "double",
153        PrimitiveKind::Float128 => "long double",
154    }
155}
156
157/// Whether the attribute is "native" in the sense of §7.4.6: a built-in number, character or
158/// boolean, or an enumeration — and not a constructed type, a string or a sequence.
159fn is_native_attribute(ast: &Ast, id: Id<TypeNode>) -> bool {
160    let mut current = id;
161    for _ in 0..MAX_DEPTH {
162        let Ok(node) = ast.type_(current) else {
163            return false;
164        };
165        match &node.kind {
166            TypeKind::Primitive(_) | TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => {
167                return true;
168            }
169            TypeKind::Alias { aliased } => match aliased.resolved() {
170                Some(next) => current = next,
171                None => return false,
172            },
173            _ => return false,
174        }
175    }
176    false
177}
178
179/// One attribute of a PSM class.
180struct Attribute {
181    /// The accessor name, keyword-escaped.
182    name: String,
183    /// The state member's name.
184    state: String,
185    /// The PSM type, with `optional`/`shared_ptr` applied.
186    type_: String,
187    /// §7.4.6: `NT` or `CT`.
188    native: bool,
189}
190
191fn attribute(ast: &Ast, member: &MemberNode, owner: &str) -> Result<Attribute, MaterialiseError> {
192    let Some(target) = member.type_.resolved() else {
193        return Err(MaterialiseError::Unevaluated {
194            construct: "member type".to_string(),
195            at: format!("{owner}::{}", member.name),
196        });
197    };
198    let inner = psm_type(ast, target, &member.name)?;
199    let name = cxx_name(&member.name);
200    // The same precedence as the native struct: an indirection first, then `@optional`.
201    let (type_, native) = match super::cpp::held(ast, member) {
202        super::cpp::Held::Shared => (format!("::dds::core::external<{inner}>"), false),
203        super::cpp::Held::Plain => (format!("::dds::core::optional<{inner}>"), false),
204        super::cpp::Held::Value => (inner, is_native_attribute(ast, target)),
205    };
206    Ok(Attribute {
207        name: name.clone(),
208        state: name,
209        type_,
210        native,
211    })
212}
213
214/// Gives each state member a name no accessor has.
215fn name_state(attributes: &mut [Attribute]) {
216    // A state name differs from every accessor and from every state name chosen before it.
217    let mut taken: Vec<String> = attributes.iter().map(|a| a.name.clone()).collect();
218    for attribute in attributes {
219        let mut state = format!("{}_", attribute.name);
220        while taken.contains(&state) {
221            state.push('_');
222        }
223        taken.push(state.clone());
224        attribute.state = state;
225    }
226}
227
228fn accessors(attribute: &Attribute) -> String {
229    let Attribute {
230        name, state, type_, ..
231    } = attribute;
232    if attribute.native {
233        format!(
234            "    {type_} {name}() const {{ return {state}; }}\n    {type_}& {name}() {{ return {state}; }}\n    void {name}({type_} value) {{ {state} = value; }}\n"
235        )
236    } else {
237        format!(
238            "    const {type_}& {name}() const {{ return {state}; }}\n    {type_}& {name}() {{ return {state}; }}\n    void {name}(const {type_}& value) {{ {state} = value; }}\n"
239        )
240    }
241}
242
243fn parameter(attribute: &Attribute) -> String {
244    if attribute.native {
245        format!("{} {}", attribute.type_, attribute.name)
246    } else {
247        format!("const {}& {}", attribute.type_, attribute.name)
248    }
249}
250
251/// The class of a structure, and the specialisation of `zerodds::psm::Conv` for it.
252fn structure(
253    ast: &Ast,
254    id: Id<TypeNode>,
255    node: &TypeNode,
256) -> Result<(String, String), MaterialiseError> {
257    let name = cxx_name(&node.name);
258    let base = ast
259        .base_of(id)
260        .filter(|_| matches!(node.kind, TypeKind::Structure { .. }));
261    let mut own = Vec::new();
262    for member in ast.members_of(id) {
263        let Ok(m) = ast.member(*member) else {
264            return Err(MaterialiseError::NoNode {
265                kind: "member",
266                at: node.name.clone(),
267            });
268        };
269        own.push(attribute(ast, m, &node.name)?);
270    }
271    name_state(&mut own);
272    // The constructor takes the base's attributes first, then its own.
273    let mut base_attributes = Vec::new();
274    if let Some(b) = base {
275        let base_node = ast.type_(b).map_err(|_| MaterialiseError::NoNode {
276            kind: "member",
277            at: node.name.clone(),
278        })?;
279        for member in ast.all_members_of(b) {
280            let Ok(m) = ast.member(member) else {
281                return Err(MaterialiseError::NoNode {
282                    kind: "member",
283                    at: base_node.name.clone(),
284                });
285            };
286            base_attributes.push(attribute(ast, m, &base_node.name)?);
287        }
288    }
289
290    let mut text = format!(
291        "/// `{}` as DDS-PSM-Cxx 1.0 \u{a7}8.1.1 maps it: a class with accessors (\u{a7}7.4.6).\nclass {name}{} {{\npublic:\n    {name}() = default;\n",
292        node.name,
293        base.map_or(String::new(), |b| format!(" : public {}", psm_path(ast, b))),
294    );
295    let parameters: Vec<String> = base_attributes
296        .iter()
297        .chain(own.iter())
298        .map(parameter)
299        .collect();
300    if !parameters.is_empty() {
301        let explicit = if parameters.len() == 1 {
302            "explicit "
303        } else {
304            ""
305        };
306        let mut initialisers = Vec::new();
307        if let Some(b) = base {
308            let args: Vec<&str> = base_attributes.iter().map(|a| a.name.as_str()).collect();
309            initialisers.push(format!("{}({})", psm_path(ast, b), args.join(", ")));
310        }
311        for a in &own {
312            initialisers.push(format!("{}({})", a.state, a.name));
313        }
314        let init = if initialisers.is_empty() {
315            String::new()
316        } else {
317            format!("\n        : {}", initialisers.join(", "))
318        };
319        text.push_str(&format!(
320            "    {explicit}{name}({}){init} {{}}\n",
321            parameters.join(", ")
322        ));
323    }
324    text.push('\n');
325    for a in &own {
326        text.push_str(&accessors(a));
327    }
328    // `swap` as a member and in the namespace, \u{a7}7.12.
329    text.push_str(&format!("\n    void swap({name}& other) {{\n"));
330    if let Some(b) = base {
331        text.push_str(&format!("        {}::swap(other);\n", psm_path(ast, b)));
332    }
333    if !own.is_empty() {
334        text.push_str("        using std::swap;\n");
335    }
336    for a in &own {
337        text.push_str(&format!("        swap({0}, other.{0});\n", a.state));
338    }
339    text.push_str("    }\n\n");
340    // Equality, member by member.
341    text.push_str(&format!(
342        "    bool operator==(const {name}& other) const {{\n        return "
343    ));
344    let mut terms: Vec<String> = Vec::new();
345    if let Some(b) = base {
346        terms.push(format!(
347            "static_cast<const {0}&>(*this) == static_cast<const {0}&>(other)",
348            psm_path(ast, b)
349        ));
350    }
351    for a in &own {
352        terms.push(format!("{0} == other.{0}", a.state));
353    }
354    if terms.is_empty() {
355        text.push_str("true");
356    } else {
357        text.push_str(&terms.join("\n               && "));
358    }
359    text.push_str(&format!(
360        ";\n    }}\n    bool operator!=(const {name}& other) const {{ return !(*this == other); }}\n\nprivate:\n"
361    ));
362    for a in &own {
363        text.push_str(&format!("    {} {}{{}};\n", a.type_, a.state));
364    }
365    text.push_str("};\n");
366    text.push_str(&format!(
367        "\ninline void swap({name}& a, {name}& b) {{ a.swap(b); }}\n"
368    ));
369
370    // The specialisation of Conv: every member, the base's first, by name.
371    let path = psm_path(ast, id);
372    let native = native_path(ast, id);
373    let mut all = Vec::new();
374    for member in ast.all_members_of(id) {
375        let Ok(m) = ast.member(member) else {
376            return Err(MaterialiseError::NoNode {
377                kind: "member",
378                at: node.name.clone(),
379            });
380        };
381        all.push(attribute(ast, m, &node.name)?);
382    }
383    let mut conv = format!(
384        "template <>\nstruct Conv<{path}> {{\n    using Native = {native};\n    static Native to_native(const {path}& v) {{\n        Native n;\n"
385    );
386    for a in &all {
387        conv.push_str(&format!(
388            "        n.{0} = Shape<decltype(n.{0}), typename Conv<{1}>::Native>::apply(Conv<{1}>::to_native(v.{0}()));\n",
389            a.name, a.type_
390        ));
391    }
392    conv.push_str(&format!(
393        "        return n;\n    }}\n    static {path} from_native(const Native& n) {{\n        {path} v;\n"
394    ));
395    for a in &all {
396        conv.push_str(&format!(
397            "        v.{0}(Conv<{1}>::from_native(Shape<typename Conv<{1}>::Native, decltype(n.{0})>::apply(n.{0})));\n",
398            a.name, a.type_
399        ));
400    }
401    conv.push_str("        return v;\n    }\n};\n");
402    Ok((text, conv))
403}
404
405/// One case of a union, for the class: its attribute, its labels and whether it is the default
406/// branch.
407struct UnionCase {
408    attribute: Attribute,
409    labels: Vec<i128>,
410    is_default: bool,
411}
412
413/// The values of the discriminator type `disc`, as numbers, when they can be listed: a
414/// `boolean`, an enumeration; `None` for an integer or character type, which has more values
415/// than any union lists.
416fn listed_values(ast: &Ast, disc: Id<TypeNode>) -> Result<Option<Vec<i128>>, MaterialiseError> {
417    let mut current = disc;
418    for _ in 0..MAX_DEPTH {
419        let Ok(node) = ast.type_(current) else {
420            return Err(MaterialiseError::NoNode {
421                kind: "discriminator type",
422                at: format!("{disc:?}"),
423            });
424        };
425        match &node.kind {
426            TypeKind::Alias { aliased } => match aliased.resolved() {
427                Some(next) => current = next,
428                None => {
429                    return Err(MaterialiseError::Unevaluated {
430                        construct: "typedef of the discriminator".to_string(),
431                        at: node.name.clone(),
432                    });
433                }
434            },
435            TypeKind::Primitive(PrimitiveKind::Boolean) => return Ok(Some(vec![0, 1])),
436            TypeKind::Enumeration { enumerators } => {
437                let mut values = Vec::new();
438                let mut next: i128 = 0;
439                for e in enumerators {
440                    let Ok(e) = ast.enumerator(*e) else {
441                        return Err(MaterialiseError::NoNode {
442                            kind: "enumerator",
443                            at: node.name.clone(),
444                        });
445                    };
446                    let value = match e.value {
447                        Some(expr) => {
448                            ast.eval_integral(expr)
449                                .map_err(|_| MaterialiseError::Unevaluated {
450                                    construct: "enumerator value".to_string(),
451                                    at: node.name.clone(),
452                                })?
453                        }
454                        None => next,
455                    };
456                    values.push(value);
457                    next = value + 1;
458                }
459                return Ok(Some(values));
460            }
461            _ => return Ok(None),
462        }
463    }
464    Ok(None)
465}
466
467/// A legal value of the discriminator that no case label names, for the default branch and for
468/// `_default()` (IDL4 to C++ 1.0 §7.2.4.3.2); `None` when the labels cover every value.
469fn unused_discriminator(
470    ast: &Ast,
471    disc: Id<TypeNode>,
472    used: &[i128],
473) -> Result<Option<i128>, MaterialiseError> {
474    if let Some(values) = listed_values(ast, disc)? {
475        return Ok(values.into_iter().find(|v| !used.contains(v)));
476    }
477    // An integer or character type: the smallest non-negative number nobody lists.
478    let mut candidate: i128 = 0;
479    while used.contains(&candidate) {
480        candidate += 1;
481    }
482    Ok(Some(candidate))
483}
484
485/// The class of a union: IDL4 to C++ 1.0 §7.2.4.3.2 (DDS-PSM-Cxx 1.0 §7.4.4 maps unions as
486/// IDL2C++11 §6.13.2 does). `_d()` and the accessors, a modifier per member that selects its
487/// case, a modifier with a `discriminator` for a member that more than one label selects, and
488/// `_default()` for an implicit default.
489fn union_class(
490    ast: &Ast,
491    id: Id<TypeNode>,
492    node: &TypeNode,
493    discriminator: &Ref<TypeNode>,
494) -> Result<(String, String), MaterialiseError> {
495    let name = cxx_name(&node.name);
496    let Some(disc) = discriminator.resolved() else {
497        return Err(MaterialiseError::Unevaluated {
498            construct: "union discriminator".to_string(),
499            at: node.name.clone(),
500        });
501    };
502    let disc_type = psm_type(ast, disc, &node.name)?;
503    let mut cases: Vec<UnionCase> = Vec::new();
504    for case in ast.cases_of(id) {
505        let Ok(case_node) = ast.case(*case) else {
506            return Err(MaterialiseError::NoNode {
507                kind: "case",
508                at: node.name.clone(),
509            });
510        };
511        let Ok(member) = ast.member(case_node.member) else {
512            return Err(MaterialiseError::NoNode {
513                kind: "member",
514                at: node.name.clone(),
515            });
516        };
517        let mut labels = Vec::new();
518        let mut is_default = false;
519        for label in &case_node.labels {
520            match label {
521                CaseLabel::Value(expr) => labels.push(ast.eval_integral(*expr).map_err(|_| {
522                    MaterialiseError::Unevaluated {
523                        construct: "union case label".to_string(),
524                        at: node.name.clone(),
525                    }
526                })?),
527                CaseLabel::Default => is_default = true,
528            }
529        }
530        cases.push(UnionCase {
531            attribute: attribute(ast, member, &node.name)?,
532            labels,
533            is_default,
534        });
535    }
536    let mut attributes: Vec<Attribute> = cases
537        .iter()
538        .map(|c| Attribute {
539            name: c.attribute.name.clone(),
540            state: c.attribute.state.clone(),
541            type_: c.attribute.type_.clone(),
542            native: c.attribute.native,
543        })
544        .collect();
545    name_state(&mut attributes);
546
547    let used: Vec<i128> = cases.iter().flat_map(|c| c.labels.clone()).collect();
548    let unused = unused_discriminator(ast, disc, &used)?;
549    let has_default_case = cases.iter().any(|c| c.is_default);
550    // The default constructor: the default branch's discriminator when there is one, else the
551    // first label of the first case (§7.2.4.3.2).
552    let initial = if has_default_case {
553        unused
554    } else {
555        cases.iter().find_map(|c| c.labels.first().copied())
556    };
557    let initial_text = initial.map_or(String::new(), |v| {
558        format!("d_ = static_cast<{disc_type}>({v});")
559    });
560
561    let mut text = format!(
562        "/// `{}`: IDL4 to C++ 1.0 \u{a7}7.2.4.3.2. A modifier selects the case of its member.\nclass {name} {{\npublic:\n    {name}() {{ {initial_text} }}\n    {name}(const {name}&) = default;\n    {name}({name}&&) = default;\n    ~{name}() = default;\n    {name}& operator=(const {name}&) = default;\n    {name}& operator=({name}&&) = default;\n\n    {disc_type} _d() const {{ return d_; }}\n    void _d({disc_type} value) {{ d_ = value; }}\n\n",
563        node.name
564    );
565    for (attribute, case) in attributes.iter().zip(cases.iter()) {
566        let (n, st, ty) = (&attribute.name, &attribute.state, &attribute.type_);
567        // The value a modifier without a discriminator selects: the first label, or the
568        // default branch's value.
569        let select_value =
570            case.labels
571                .first()
572                .copied()
573                .or(if case.is_default { unused } else { None });
574        let select = select_value.map_or(String::new(), |value| {
575            format!(" d_ = static_cast<{disc_type}>({value});")
576        });
577        let multiple = case.labels.len() > 1;
578        if attribute.native {
579            text.push_str(&format!(
580                "    {ty} {n}() const {{ return {st}; }}\n    {ty}& {n}() {{ return {st}; }}\n    void {n}({ty} value) {{ {st} = value;{select} }}\n"
581            ));
582            if multiple {
583                text.push_str(&format!(
584                    "    void {n}({ty} value, {disc_type} discriminator) {{ {st} = value; d_ = discriminator; }}\n"
585                ));
586            }
587        } else {
588            text.push_str(&format!(
589                "    const {ty}& {n}() const {{ return {st}; }}\n    {ty}& {n}() {{ return {st}; }}\n    void {n}(const {ty}& value) {{ {st} = value;{select} }}\n    void {n}({ty}&& value) {{ {st} = std::move(value);{select} }}\n"
590            ));
591            if multiple {
592                text.push_str(&format!(
593                    "    void {n}(const {ty}& value, {disc_type} discriminator) {{ {st} = value; d_ = discriminator; }}\n    void {n}({ty}&& value, {disc_type} discriminator) {{ {st} = std::move(value); d_ = discriminator; }}\n"
594                ));
595            }
596        }
597    }
598    // An implicit default member: no default case and labels that leave a value out.
599    if !has_default_case {
600        if let Some(value) = unused {
601            text.push_str(&format!(
602                "\n    void _default() {{ d_ = static_cast<{disc_type}>({value}); }}\n"
603            ));
604        }
605    }
606    text.push_str(&format!(
607        "\n    void swap({name}& other) {{\n        using std::swap;\n        swap(d_, other.d_);\n"
608    ));
609    for attribute in &attributes {
610        text.push_str(&format!("        swap({0}, other.{0});\n", attribute.state));
611    }
612    text.push_str("    }\n");
613    text.push_str(&format!(
614        "\n    bool operator==(const {name}& other) const {{\n        return d_ == other.d_"
615    ));
616    for attribute in &attributes {
617        text.push_str(&format!(" && {0} == other.{0}", attribute.state));
618    }
619    text.push_str(&format!(
620        ";\n    }}\n    bool operator!=(const {name}& other) const {{ return !(*this == other); }}\n\nprivate:\n    {disc_type} d_{{}};\n"
621    ));
622    for attribute in &attributes {
623        text.push_str(&format!(
624            "    {} {}{{}};\n",
625            attribute.type_, attribute.state
626        ));
627    }
628    text.push_str(&format!(
629        "}};\n\ninline void swap({name}& a, {name}& b) {{ a.swap(b); }}\n"
630    ));
631
632    let path = psm_path(ast, id);
633    let native = native_path(ast, id);
634    // The cases first, whose setters select their own discriminator value, and the
635    // discriminator last, which is the one the value had.
636    let mut conv = format!(
637        "template <>\nstruct Conv<{path}> {{\n    using Native = {native};\n    static Native to_native(const {path}& v) {{\n        Native n;\n"
638    );
639    for case in &cases {
640        conv.push_str(&format!(
641            "        n.{0}(Shape<std::remove_cv_t<std::remove_reference_t<decltype(n.{0}())>>, typename Conv<{1}>::Native>::apply(Conv<{1}>::to_native(v.{0}())));\n",
642            case.attribute.name, case.attribute.type_
643        ));
644    }
645    // Only the arm the discriminator selects is read: the native codec fills that one, and the
646    // others of a native union are not initialised. A modifier selects its case, so the
647    // discriminator is set last.
648    conv.push_str(&format!(
649        "        n._d(Conv<{disc_type}>::to_native(v._d()));\n        return n;\n    }}\n    static {path} from_native(const Native& n) {{\n        {path} v;\n        switch (static_cast<long long>(n._d())) {{\n"
650    ));
651    for case in &cases {
652        let line = format!(
653            "            v.{0}(Conv<{1}>::from_native(Shape<typename Conv<{1}>::Native, std::remove_cv_t<std::remove_reference_t<decltype(n.{0}())>>>::apply(n.{0}())));\n            break;\n",
654            case.attribute.name, case.attribute.type_
655        );
656        if case.is_default {
657            conv.push_str("            default:\n");
658        }
659        for label in &case.labels {
660            conv.push_str(&format!("            case {label}LL:\n"));
661        }
662        if !case.labels.is_empty() || case.is_default {
663            conv.push_str(&line);
664        }
665    }
666    if !has_default_case {
667        conv.push_str("            default:\n                break;\n");
668    }
669    conv.push_str(&format!(
670        "        }}\n        v._d(Conv<{disc_type}>::from_native(n._d()));\n        return v;\n    }}\n}};\n"
671    ));
672    Ok((text, conv))
673}
674
675/// `namespace a { namespace b { … } }` around `text`.
676pub(crate) fn in_namespaces(chain: &[String], text: &str) -> String {
677    let mut out = String::new();
678    for name in chain {
679        out.push_str(&format!("namespace {name} {{\n"));
680    }
681    out.push_str(text);
682    if !text.ends_with('\n') {
683        out.push('\n');
684    }
685    for _ in chain {
686        out.push_str("}\n");
687    }
688    out
689}
690
691/// The text of the PSM layer for `ast`: forward declarations, the types in dependency order,
692/// and the `zerodds::psm::Conv` specialisations.
693///
694/// # Errors
695/// A construct the PSM spelling has no form for, or a model that contradicts itself.
696pub fn psm_layer(ast: &Ast) -> Result<String, MaterialiseError> {
697    let order = ordered_types(ast);
698    // A model with a pair of types takes the common types of DDS-RPC 1.0 \u{a7}7.5.1.1.1 from the
699    // runtime (`dds::SampleIdentity`, `dds::rpc::RequestHeader`, ...): no PSM class, no
700    // `swap` and no conversion for them, as the native declarations are dropped too.
701    let pair = crate::emit::type_pair_cpp::has_pair_types(ast);
702    let runtime_defined = |node: &TypeNode| -> Result<bool, MaterialiseError> {
703        Ok(pair
704            && crate::emit::type_pair_cpp::is_runtime_defined(
705                &module_names(ast, node.module)?,
706                &node.name,
707            ))
708    };
709    let mut forward = String::new();
710    let mut types = String::new();
711    let mut conversions = String::new();
712
713    for id in &order {
714        let Ok(node) = ast.type_(*id) else {
715            return Err(no_form(
716                "a type the model would not hand over",
717                "forward declarations",
718            ));
719        };
720        if node.name.is_empty() || runtime_defined(node)? {
721            continue;
722        }
723        if matches!(
724            node.kind,
725            TypeKind::Structure { .. } | TypeKind::Union { .. }
726        ) {
727            let chain = module_names(ast, node.module)?;
728            forward.push_str(&in_namespaces(
729                &chain,
730                &format!("class {};", cxx_name(&node.name)),
731            ));
732        }
733    }
734    for id in order {
735        let Ok(node) = ast.type_(id) else {
736            return Err(no_form(
737                "a type the model would not hand over",
738                "type declarations",
739            ));
740        };
741        if node.name.is_empty() || runtime_defined(node)? {
742            continue;
743        }
744        let chain = module_names(ast, node.module)?;
745        let name = cxx_name(&node.name);
746        let (text, conv) = match &node.kind {
747            TypeKind::Structure { .. } => {
748                let (class, conv) = structure(ast, id, node)?;
749                (class, Some(conv))
750            }
751            TypeKind::Union { discriminator, .. } => {
752                let (class, conv) = union_class(ast, id, node, discriminator)?;
753                (class, Some(conv))
754            }
755            TypeKind::Enumeration { .. }
756            | TypeKind::Bitmask { .. }
757            | TypeKind::Bitset { .. }
758            | TypeKind::Native => (
759                format!(
760                    "/// Declared once, with the IDL's name and constants (DDS-PSM-Cxx 1.0 \u{a7}8.1.3).\nusing {name} = {};\n",
761                    native_path(ast, id)
762                ),
763                None,
764            ),
765            TypeKind::Alias { aliased } => {
766                let Some(target) = aliased.resolved() else {
767                    return Err(no_form("an unresolved typedef", &node.name));
768                };
769                (
770                    format!("using {name} = {};\n", psm_type(ast, target, &node.name)?),
771                    None,
772                )
773            }
774            TypeKind::ValueBox { boxed } => {
775                let Some(target) = boxed.resolved() else {
776                    return Err(no_form("an unresolved boxed value", &node.name));
777                };
778                (
779                    format!(
780                        "using {name} = std::shared_ptr<{}>;\n",
781                        psm_type(ast, target, &node.name)?
782                    ),
783                    None,
784                )
785            }
786            // Spellings of anonymous constructs (a typedef names them), `any`, and annotation
787            // declarations, which carry no data: nothing to declare.
788            TypeKind::Primitive(_)
789            | TypeKind::Fixed { .. }
790            | TypeKind::String { .. }
791            | TypeKind::Sequence { .. }
792            | TypeKind::Array { .. }
793            | TypeKind::Map { .. }
794            | TypeKind::Any
795            | TypeKind::AnnotationType { .. } => continue,
796            // DDS-PSM-Cxx 1.0 \u{a7}8.1 has no mapping for these; saying so beats leaving the
797            // name out of the PSM layer.
798            TypeKind::Valuetype { .. } | TypeKind::ValueBase | TypeKind::ObjectRef { .. } => {
799                return Err(no_form(
800                    "a valuetype, interface or ValueBase has no DDS-PSM-Cxx 1.0 mapping",
801                    &node.name,
802                ));
803            }
804        };
805        types.push_str(&in_namespaces(&chain, &text));
806        types.push('\n');
807        if let Some(conv) = conv {
808            conversions.push_str(&conv);
809            conversions.push('\n');
810        }
811    }
812    // An exception is not remapped (DDS-PSM-Cxx 1.0 \u{a7}8.1 has none): the application throws and
813    // catches the native class, under the IDL's own name.
814    for (_, exception) in ast.exceptions() {
815        let chain = module_names(ast, exception.module)?;
816        let name = cxx_name(&exception.name);
817        let mut native = format!("::{NATIVE_NAMESPACE}");
818        for segment in &chain {
819            native.push_str("::");
820            native.push_str(segment);
821        }
822        native.push_str("::");
823        native.push_str(&name);
824        types.push_str(&in_namespaces(
825            &chain,
826            &format!("using {name} = {native};\n"),
827        ));
828        types.push('\n');
829    }
830    // A model that declares no type has nothing to layer: no text, so that no preamble is written
831    // for it (the model, not the flag, decides whether the file names the runtime).
832    if types.is_empty() {
833        return Ok(String::new());
834    }
835    Ok(format!(
836        "// ---- The PSM layer: DDS-PSM-Cxx 1.0 \u{a7}8.1 over the native types above. ----\n\
837         // The native types are in `{NATIVE_NAMESPACE}`; these classes are what an application writes.\n\
838         // `zerodds::psm::Conv` hands them to the codec.\n\n{forward}\n{types}\
839         namespace zerodds {{\nnamespace psm {{\n\n{conversions}}} // namespace psm\n}} // namespace zerodds\n"
840    ))
841}
842
843#[cfg(test)]
844mod tests {
845    use super::*;
846
847    #[test]
848    fn a_name_that_is_an_accessor_is_not_a_state_member() {
849        let mut attributes = vec![
850            Attribute {
851                name: "a".into(),
852                state: String::new(),
853                type_: "int32_t".into(),
854                native: true,
855            },
856            Attribute {
857                name: "a_".into(),
858                state: String::new(),
859                type_: "int32_t".into(),
860                native: true,
861            },
862        ];
863        name_state(&mut attributes);
864        assert_eq!(attributes[0].state, "a__");
865        assert_eq!(attributes[1].state, "a___");
866    }
867
868    #[test]
869    fn the_primitives_are_the_table_7_1_names() {
870        assert_eq!(primitive(PrimitiveKind::WChar), "wchar_t");
871        assert_eq!(primitive(PrimitiveKind::Float128), "long double");
872        assert_eq!(primitive(PrimitiveKind::Octet), "uint8_t");
873    }
874}