Skip to main content

zerodds_lang_rules/adapter/
interface.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! An IDL `interface`, as the operations a language binding needs.
4//!
5//! The struct columns have a seam already: `adapter::cst::rust_items`,
6//! `adapter::cpp::cpp_items` and `adapter::typescript::ts_items` each
7//! walk `ast.types()` and turn one model node into one declaration. An
8//! `interface` had no such walk. `adapter::ast` puts the node into the
9//! graph and `rule::{cpp,java,typescript}` each carry an
10//! `interface-to-…` rule that fires on it, and then nothing read the
11//! result: `cpp_items` says so in as many words, and the other two
12//! simply do not look. Every RPC language binding therefore had a rule
13//! that matched and no output.
14//!
15//! This is the missing half, and it is deliberately language-neutral.
16//! What a C++ binding needs from an `interface` — the operations, each
17//! parameter's direction, the return type, the `raises` list, the
18//! attributes and the inheritance chain — is what a Java binding needs
19//! and what a TypeScript binding needs. Written three times it would be
20//! three readings of OMG IDL 4.2 §7.4.3, and the columns would disagree
21//! about which parameters land in a request before anyone noticed.
22//!
23//! ## What it is not
24//!
25//! Not the wire model. `crates/rpc/src/codegen.rs` derives the request
26//! and reply topic types from DDS-RPC 1.0 §7.5.1 and is the authority on
27//! their shape. This module states the *names* that mapping fixes —
28//! [`request_type_name`] and the four beside it — so a generated header
29//! and the runtime that has to read it agree, and
30//! `crates/rpc/tests/language_binding_agreement.rs` holds the two
31//! against each other rather than trusting that they were written from
32//! the same paragraph.
33//!
34//! Not a dependency on `zerodds-rpc` either, which would be a cycle:
35//! `zerodds-rpc` → `zerodds-dcps` → `zerodds-lang-rules`.
36//!
37//! ## Spec
38//!
39//! - OMG IDL 4.2 §7.4.3 — `<interface_dcl>`, `<op_dcl>`, `<attr_dcl>`.
40//! - DDS-RPC 1.0 §7.5.1.1.3 — an attribute's implied operations.
41//! - DDS-RPC 1.0 §7.5.1.1.4-8 — the request and reply topic types.
42//! - DDS-RPC 1.0 §7.11 — the C++ and Java language bindings.
43//! - DDS-TS 1.0 Ch11 — the TypeScript client/handler mapping.
44
45use zerodds_ast::{
46    Ast, AttributeNode, ExceptionNode, InterfaceKind, InterfaceNode, ModuleNode, OperationNode,
47    ParamDirection, TypeNode,
48};
49use zerodds_ast_core::{Id, NodeId};
50
51/// The member an `Out` structure carries an operation's return value
52/// in. DDS-RPC 1.0 §7.5.1.1.5 rule 3b.
53///
54/// `return_` with the underscore trailing, which is what the clause
55/// prints in its own non-normative example.
56/// `crates/rpc/src/codegen.rs::method_out_members` writes the same name,
57/// so a binding that spelled it differently would generate a reader for
58/// a member the writer never wrote.
59pub const RETURN_MEMBER: &str = "return_";
60
61/// The member an `In` or `Out` structure carries when the operation
62/// gives it nothing else. §7.5.1.1.4 rule 3c, §7.5.1.1.5 rule 3c.
63pub const DUMMY_MEMBER: &str = "dummy";
64
65/// The member the `default` case of a `Call` or `Return` union carries.
66/// §7.5.1.1.6 rule 3.
67pub const UNKNOWN_OP_MEMBER: &str = "unknownOp";
68
69/// The member the `dds::RETCODE_OK` case of a `Result` union carries.
70/// §7.5.1.1.5 rule 3a.
71pub const RESULT_MEMBER: &str = "result";
72
73/// The member a `Request` and a `Reply` structure carry their union in.
74/// §7.5.1.1.6 rule 2b, §7.5.1.1.7 rule 3b.
75pub const DATA_MEMBER: &str = "data";
76
77/// The label of the `Result` union's success case. DDS-DCPS gives
78/// `RETCODE_OK` the value 0.
79pub const RETCODE_OK: i32 = 0;
80
81/// The suffix a request topic and its topic type take. DDS-RPC 1.0
82/// §7.4.1, §7.5.1.1.6.
83pub const REQUEST_SUFFIX: &str = "_Request";
84
85/// The suffix a reply topic and its topic type take. DDS-RPC 1.0 §7.4.1,
86/// §7.5.1.1.7.
87pub const REPLY_SUFFIX: &str = "_Reply";
88
89/// The `<service_name>` a service takes when the IDL supplies none.
90///
91/// DDS-RPC 1.0 §7.4.1: "The `<service_name>` non-terminal may be
92/// user-supplied. When it is not, it defaults to 'Service.'"
93pub const DEFAULT_SERVICE_NAME: &str = "Service";
94
95/// Why an interface did not reach the seam.
96///
97/// Four failures, each of which used to be a construct that reached no
98/// output and said nothing about it.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum InterfaceError {
101    /// `interface I;` with no `interface I { … };` anywhere in the unit.
102    ///
103    /// DDS-TS 1.0 §11.7 makes this fatal and names it `DDS-TS-E002`.
104    /// The C++ and Java bindings have the same hole for the same
105    /// reason: a `Requester<I_Request, I_Reply>` over an interface whose
106    /// operations never arrived is a class over two structs with no
107    /// members.
108    ForwardWithoutDefinition {
109        /// The interface's IDL name.
110        name: String,
111    },
112    /// The model lists a child and then will not hand it over.
113    NoNode {
114        /// What was asked for: `"operation"`, `"parameter"`,
115        /// `"attribute"`, `"base interface"` or `"exception"`.
116        kind: &'static str,
117        /// Where it sits, as far as the model names it.
118        at: String,
119    },
120    /// A reference the output has to name is not resolved.
121    ///
122    /// A return type, a parameter type, an attribute type, a base
123    /// interface or a `raises` entry. Emitting the operation without it
124    /// would produce a method with a parameter missing, which no
125    /// compiler on the far side can tell from one the IDL declared that
126    /// way.
127    Unresolved {
128        /// What does not resolve, in the source's terms.
129        construct: &'static str,
130        /// Where it sits, as far as the model names it.
131        at: String,
132    },
133    /// The base chain is deeper than the walk follows.
134    ///
135    /// Stopping there quietly would drop the operations and attributes
136    /// of every base past the limit from the flattened binding.
137    TooDeep {
138        /// The interface whose chain exceeded the limit.
139        name: String,
140        /// The limit.
141        limit: usize,
142    },
143    /// A construct DDS-RPC 1.0 §7.3.1.1 removed from the service
144    /// grammar: a `oneway` operation (production 87 and §7.5.1.1.7
145    /// rule 5, which wants a Return-union case for every operation), a
146    /// `context` clause, an interface form other than `interface`
147    /// (production 7), an `import`/`typeid`/`typeprefix` directive
148    /// (productions 1 and 2), or a type, constant or exception
149    /// declaration in an interface body (production 9).
150    OutsideServiceGrammar {
151        /// The construct, in the spec's terms.
152        construct: &'static str,
153        /// Where it sits.
154        at: String,
155    },
156    /// Two operations of one interface carry the same name. Covers the
157    /// operations §7.5.1.1.3 rule 1 implies for an attribute: "It is
158    /// illegal to have an interface with an attribute named
159    /// `<attribute-name>` and user-defined operations named
160    /// `get_attribute_<attribute-name>` and
161    /// `set_attribute_<attribute-name>`."
162    DuplicateOperation {
163        /// The interface.
164        interface: String,
165        /// The colliding operation name.
166        name: String,
167    },
168    /// Two parameters of one operation carry the same name.
169    DuplicateParameter {
170        /// `<interface>::<operation>`.
171        at: String,
172        /// The colliding parameter name.
173        name: String,
174    },
175}
176
177impl core::fmt::Display for InterfaceError {
178    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
179        match self {
180            Self::ForwardWithoutDefinition { name } => write!(
181                f,
182                "interface `{name}` is forward-declared and never defined"
183            ),
184            Self::NoNode { kind, at } => {
185                write!(f, "{at}: the model lists a {kind} it will not hand over")
186            }
187            Self::Unresolved { construct, at } => {
188                write!(f, "{at}: the {construct} does not resolve")
189            }
190            Self::TooDeep { name, limit } => write!(
191                f,
192                "interface `{name}`: the inheritance chain is deeper than {limit} levels"
193            ),
194            Self::OutsideServiceGrammar { construct, at } => write!(
195                f,
196                "{at}: {construct} is outside the DDS-RPC 1.0 §7.3.1.1 service grammar"
197            ),
198            Self::DuplicateOperation { interface, name } => write!(
199                f,
200                "interface `{interface}`: the operation name `{name}` is used twice \
201                 (DDS-RPC 1.0 §7.5.1.1.3 rule 1 covers the names an attribute implies)"
202            ),
203            Self::DuplicateParameter { at, name } => {
204                write!(f, "{at}: the parameter name `{name}` is used twice")
205            }
206        }
207    }
208}
209
210impl core::error::Error for InterfaceError {}
211
212/// Wraps an interface-level failure as the one error type a
213/// materialiser already returns, for a flattened-interface binding that
214/// reads [`all_operations_flattened`]/[`all_attributes_flattened`]
215/// straight from its own `interfaces` function and has no second error
216/// type of its own to keep it in. `at` is the interface's own name —
217/// the caller's loop already has it, and it is a better answer than
218/// anything this function could derive from `error` alone.
219#[must_use]
220pub fn interface_error_to_materialise(
221    error: InterfaceError,
222    at: &str,
223) -> crate::adapter::cst::MaterialiseError {
224    crate::adapter::cst::MaterialiseError::Unevaluated {
225        construct: format!("interface binding ({error})"),
226        at: at.to_string(),
227    }
228}
229
230/// Why an interface did not become a language binding.
231///
232/// Two halves, because two questions are being asked. [`InterfaceError`]
233/// is about the interface: is it defined, do its references resolve.
234/// [`crate::adapter::cst::MaterialiseError`] is about a type inside it:
235/// does the target language have a form for a `sequence<any>`. A binding
236/// that folded the two would report an unresolved parameter type and a
237/// language with no spelling for `any` as the same failure.
238#[derive(Debug, Clone, PartialEq, Eq)]
239pub enum BindingError {
240    /// The interface did not reach the seam.
241    Interface(InterfaceError),
242    /// A type in it has no form in the target language.
243    Materialise(crate::adapter::cst::MaterialiseError),
244    /// A codec for the topic types of the interface could not be built,
245    /// with the reason.
246    Codec(String),
247}
248
249impl BindingError {
250    /// The diagnostic code DDS-TS 1.0 Annex D gives this failure, if it
251    /// gives one.
252    ///
253    /// Only §11.7's orphaned forward declaration has one: `DDS-TS-E002`,
254    /// "forward-declared interface lacks complete declaration". Every
255    /// other failure here is a refusal the code registry does not name.
256    #[must_use]
257    pub const fn diagnostic_code(&self) -> Option<&'static str> {
258        match self {
259            Self::Interface(InterfaceError::ForwardWithoutDefinition { .. }) => Some("DDS-TS-E002"),
260            Self::Interface(_) | Self::Materialise(_) | Self::Codec(_) => None,
261        }
262    }
263}
264
265impl From<InterfaceError> for BindingError {
266    fn from(error: InterfaceError) -> Self {
267        Self::Interface(error)
268    }
269}
270
271impl From<crate::adapter::cst::MaterialiseError> for BindingError {
272    fn from(error: crate::adapter::cst::MaterialiseError) -> Self {
273        Self::Materialise(error)
274    }
275}
276
277impl core::fmt::Display for BindingError {
278    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
279        match self {
280            Self::Interface(error) => write!(f, "{error}"),
281            Self::Materialise(error) => write!(f, "{error}"),
282            Self::Codec(reason) => write!(f, "no codec for the topic types: {reason}"),
283        }
284    }
285}
286
287impl core::error::Error for BindingError {}
288
289/// One entry of an operation's `raises` clause, or of an attribute's
290/// `getraises` / `setraises`.
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct Raise {
293    /// The exception's handle.
294    pub id: Id<ExceptionNode>,
295    /// Its simple IDL name, which §7.5.1.1.5 rule 5b lower-cases into
296    /// the union member's.
297    pub name: String,
298    /// Its scoped name, `::`-separated — what §7.5.1.1.5 rule 4b
299    /// hashes: "the HASH of the fully qualified name of the exception
300    /// type where module separator to be used is `::` (2 colons)".
301    pub scoped_name: String,
302}
303
304/// One parameter of an operation. OMG IDL 4.2 §7.4.3, rule (86).
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct Parameter {
307    /// Its IDL name.
308    pub name: String,
309    /// `in`, `out` or `inout`.
310    pub direction: ParamDirection,
311    /// Its declared type.
312    pub type_: Id<TypeNode>,
313}
314
315impl Parameter {
316    /// Whether this parameter travels in the request. DDS-RPC 1.0
317    /// §7.5.1.1.4: `in` and `inout`.
318    #[must_use]
319    pub const fn is_input(&self) -> bool {
320        matches!(self.direction, ParamDirection::In | ParamDirection::InOut)
321    }
322
323    /// Whether this parameter travels in the reply. DDS-RPC 1.0
324    /// §7.5.1.1.5: `out` and `inout`.
325    #[must_use]
326    pub const fn is_output(&self) -> bool {
327        matches!(self.direction, ParamDirection::Out | ParamDirection::InOut)
328    }
329}
330
331/// Where an operation came from.
332///
333/// An IDL `interface` declares operations directly and declares
334/// attributes, and DDS-RPC 1.0 §7.5.1.1.3 turns each attribute into one
335/// or two *implied* operations. The two are the same shape on the wire
336/// and are not the same thing in a generated header — a C++ attribute
337/// getter is spelled per DDS-Cxx-PSM §7.4.6 and a declared operation is
338/// spelled per §7.11.1.1.2 rule 6 — so the origin travels with the
339/// operation rather than being guessed from the name.
340#[derive(Debug, Clone, Copy, PartialEq, Eq)]
341pub enum Origin {
342    /// Written as `<op_dcl>` in the IDL.
343    Declared,
344    /// The getter DDS-RPC 1.0 §7.5.1.1.3 implies for an attribute.
345    AttributeGetter,
346    /// The setter DDS-RPC 1.0 §7.5.1.1.3 implies for a writable
347    /// attribute.
348    AttributeSetter,
349}
350
351/// One operation of an interface. OMG IDL 4.2 §7.4.3, rule (82).
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct Operation {
354    /// Stable identity of the model node, absent for an implied
355    /// operation, which has no `<op_dcl>` of its own.
356    pub node_id: Option<NodeId>,
357    /// Its IDL name, or the implied name of §7.5.1.1.3.
358    pub name: String,
359    /// The return type, absent for `void`.
360    pub returns: Option<Id<TypeNode>>,
361    /// Parameters in declaration order.
362    pub params: Vec<Parameter>,
363    /// The `raises` clause, in declaration order.
364    pub raises: Vec<Raise>,
365    /// Whether the IDL wrote it or §7.5.1.1.3 implied it.
366    pub origin: Origin,
367    /// Whether it carries `@Mutable` (DDS-RPC 1.0 §7.5.1.2.1.4): the
368    /// Enhanced Service Mapping declares its `In`, `Out` and `Result`
369    /// structures `@Extensibility(MUTABLE_EXTENSIBILITY)`. The Basic
370    /// mapping ignores it.
371    pub mutable: bool,
372}
373
374impl Operation {
375    /// The attribute an implied accessor belongs to: the operation name
376    /// without the `get_attribute_` / `set_attribute_` prefix of DDS-RPC 1.0
377    /// §7.5.1.1.3 rule 1. For a declared operation, its own name.
378    #[must_use]
379    pub fn attribute_name(&self) -> &str {
380        match self.origin {
381            Origin::AttributeGetter => self.name.strip_prefix("get_attribute_"),
382            Origin::AttributeSetter => self.name.strip_prefix("set_attribute_"),
383            Origin::Declared => None,
384        }
385        .unwrap_or(&self.name)
386    }
387
388    /// The parameters a request carries, in declaration order.
389    pub fn inputs(&self) -> impl Iterator<Item = &Parameter> {
390        self.params.iter().filter(|param| param.is_input())
391    }
392
393    /// The parameters a reply carries, in declaration order.
394    pub fn outputs(&self) -> impl Iterator<Item = &Parameter> {
395        self.params.iter().filter(|param| param.is_output())
396    }
397
398    /// The `raises` clause, as one line of prose, or `None` where the
399    /// operation raises nothing.
400    ///
401    /// DDS-RPC 1.0 §7.11.1.1.2 rule 6.c keeps the clause out of the C++
402    /// signature — "The functions shall not have any exception
403    /// specification even if the IDL operation has an exception
404    /// specification" — and §7.11.2 does not override it for Java. It
405    /// does not say to hide what the IDL declared, and a generated
406    /// method with neither a `throws` clause nor a line naming the
407    /// exceptions would be the one place in the pipeline where a
408    /// declared `raises` reaches no output at all.
409    #[must_use]
410    pub fn raises_line(&self) -> Option<String> {
411        raises_line(&self.raises)
412    }
413
414    /// Whether the reply carries anything beside the return value.
415    ///
416    /// DDS-RPC 1.0 §7.11.1.1.2 rule 7.i turns on this: an operation with
417    /// `out` or `inout` parameters returns
418    /// `future<${interface}_${operation}_Out>` and one without returns
419    /// `future<T>` or `future<void>`.
420    #[must_use]
421    pub fn has_outputs(&self) -> bool {
422        self.params.iter().any(Parameter::is_output)
423    }
424}
425
426/// One attribute of an interface. OMG IDL 4.2 §7.4.3, rule (88).
427#[derive(Debug, Clone, PartialEq, Eq)]
428pub struct Attribute {
429    /// Stable identity of the model node.
430    pub node_id: NodeId,
431    /// Its IDL name.
432    pub name: String,
433    /// Its declared type.
434    pub type_: Id<TypeNode>,
435    /// Whether it is `readonly`.
436    pub readonly: bool,
437    /// The `getraises` clause, in declaration order.
438    pub get_raises: Vec<Raise>,
439    /// The `setraises` clause, in declaration order.
440    pub set_raises: Vec<Raise>,
441}
442
443impl Attribute {
444    /// [`raises_line`] over [`Self::get_raises`], for a language that
445    /// renders the getter half of the attribute separately from the
446    /// setter (a native property's `get` accessor, or a getter method).
447    #[must_use]
448    pub fn get_raises_line(&self) -> Option<String> {
449        raises_line(&self.get_raises)
450    }
451
452    /// [`raises_line`] over [`Self::set_raises`].
453    #[must_use]
454    pub fn set_raises_line(&self) -> Option<String> {
455        raises_line(&self.set_raises)
456    }
457}
458
459/// A `raises`/`getraises`/`setraises` clause, as one line of prose, or
460/// `None` where it is empty. [`Operation::raises_line`] and
461/// [`Attribute::get_raises_line`]/[`Attribute::set_raises_line`] all
462/// call this, so the wording is one decision and not three.
463///
464/// DDS-RPC 1.0 §7.11.1.1.2 rule 6.c keeps the clause out of the C++
465/// signature — "The functions shall not have any exception
466/// specification even if the IDL operation has an exception
467/// specification" — and §7.11.2 does not override it for Java. It does
468/// not say to hide what the IDL declared, and a generated member with
469/// neither a `throws` clause nor a line naming the exceptions would be
470/// the one place in the pipeline where a declared `raises` reaches no
471/// output at all.
472#[must_use]
473pub fn raises_line(raises: &[Raise]) -> Option<String> {
474    if raises.is_empty() {
475        return None;
476    }
477    let names: Vec<&str> = raises.iter().map(|raise| raise.name.as_str()).collect();
478    Some(format!(
479        "Raises, as the IDL declares it: {}. DDS-RPC 1.0 §7.11.1.1.2 rule 6.c keeps it out of \
480         the signature.",
481        names.join(", ")
482    ))
483}
484
485/// A base interface, as the inheritance specification names it. OMG IDL
486/// 4.2 §7.4.3, rule (78).
487#[derive(Debug, Clone, PartialEq, Eq)]
488pub struct Base {
489    /// The base's handle.
490    pub id: Id<InterfaceNode>,
491    /// Its simple IDL name.
492    pub name: String,
493    /// Its `<interface_name>` of §7.4.1, which §7.4.3 turns into a
494    /// topic alias.
495    pub qualified_name: String,
496}
497
498/// One IDL `interface`, with everything a language binding reads off it.
499#[derive(Debug, Clone, PartialEq, Eq)]
500pub struct Service {
501    /// The interface's handle.
502    pub id: Id<InterfaceNode>,
503    /// Stable identity of the model node.
504    pub node_id: NodeId,
505    /// Its IDL name.
506    pub name: String,
507    /// The module it is declared in.
508    pub module: Id<ModuleNode>,
509    /// The `<interface_name>` of DDS-RPC 1.0 §7.4.1: "concatenation of
510    /// module names separates by underscores followed by the name of the
511    /// interface".
512    ///
513    /// Distinct from [`Self::name`], and the distinction is a wire one:
514    /// §7.5.1.1.6 builds the topic *type* names from the simple name
515    /// (`Calculator_Request`) and §7.4.1 builds the *topic* names from
516    /// this one (`robot_Calculator_Service_Request`). A binding that
517    /// used one for both publishes a type nobody subscribes to.
518    pub qualified_name: String,
519    /// The `<service_name>` of §7.4.1 — what `@service(name = "…")`
520    /// supplies, or [`DEFAULT_SERVICE_NAME`].
521    pub service_name: String,
522    /// `@DDSRequestTopic(name = "…")`, §7.4.2.2 — a whole topic name.
523    /// "These annotations simply control the generated DDS wrapper
524    /// code", so the generator is where they are read.
525    pub request_topic: Option<String>,
526    /// `@DDSReplyTopic(name = "…")`, §7.4.2.2.
527    pub reply_topic: Option<String>,
528    /// `@Qos(RequestProfile = "…")`, §7.10.1 — the URL of the XML QoS
529    /// profile for the request endpoints.
530    pub qos_request_profile: Option<String>,
531    /// `@Qos(ReplyProfile = "…")`, §7.10.1 — the URL for the reply
532    /// endpoints.
533    pub qos_reply_profile: Option<String>,
534    /// Which interface form the IDL wrote.
535    pub kind: InterfaceKind,
536    /// Base interfaces, in declaration order.
537    pub bases: Vec<Base>,
538    /// Operations, in declaration order.
539    pub operations: Vec<Operation>,
540    /// Attributes, in declaration order.
541    pub attributes: Vec<Attribute>,
542    /// The service mapping a binding is written for. [`service`] reads
543    /// [`ServiceMapping::Basic`], the default; a generator that writes
544    /// the Enhanced mapping sets it on the services it passes down, so
545    /// that the pieces shared by the two mappings — the client and the
546    /// dispatcher — know which of the two they bind.
547    pub mapping: ServiceMapping,
548}
549
550impl Service {
551    /// Whether this form has a remote invocation at all.
552    ///
553    /// A `local interface` (OMG IDL 4.2 §7.4.3.2) is by definition not
554    /// invocable across a boundary, so a `Requester` over it would be a
555    /// class whose whole purpose the declaration rules out. The abstract
556    /// interface and the async interface are still emitted for it — it
557    /// has operations, and a caller may hold one — which is why this is
558    /// a question about the endpoints and not about the whole binding.
559    ///
560    /// The CCM forms (`component`, `home`, `connector`, `porttype`) have
561    /// their own mapping in CCM 4.0 §6 and are not DDS-RPC services.
562    #[must_use]
563    pub const fn is_remote(&self) -> bool {
564        matches!(
565            self.kind,
566            InterfaceKind::Interface | InterfaceKind::Abstract
567        )
568    }
569
570    /// The request topic's name under `style`. DDS-RPC 1.0 §7.4.1, with
571    /// §7.4.2.2's annotation where the IDL wrote one.
572    ///
573    /// The run-time layer of §7.4.2.3 is not here and cannot be: a
574    /// generated header is written before there is a run time. What it
575    /// carries is §7.4.2.1's default and §7.4.2.2's annotation, and the
576    /// run time overrides both through
577    /// `crates/rpc/src/topic_naming.rs::TopicNameOverrides`.
578    #[must_use]
579    pub fn request_topic_name(&self, style: BindingStyle) -> String {
580        self.request_topic.clone().unwrap_or_else(|| {
581            compose_topic_name(
582                style,
583                &self.qualified_name,
584                &format!("{}{REQUEST_SUFFIX}", self.service_name),
585            )
586        })
587    }
588
589    /// The reply topic's name under `style`. See
590    /// [`Self::request_topic_name`].
591    #[must_use]
592    pub fn reply_topic_name(&self, style: BindingStyle) -> String {
593        self.reply_topic.clone().unwrap_or_else(|| {
594            compose_topic_name(
595                style,
596                &self.qualified_name,
597                &format!("{}{REPLY_SUFFIX}", self.service_name),
598            )
599        })
600    }
601
602    /// §7.4.3's topic aliases: one per parent interface, same tail.
603    ///
604    /// Empty where the topic name came from an annotation — a whole name
605    /// has no interface segment to vary — and empty under the
606    /// request-reply binding, where the name carries none either.
607    #[must_use]
608    pub fn topic_aliases(&self, style: BindingStyle, reply: bool) -> Vec<String> {
609        let annotated = if reply {
610            self.reply_topic.is_some()
611        } else {
612            self.request_topic.is_some()
613        };
614        if annotated || !style.captures_interface_name() {
615            return Vec::new();
616        }
617        let suffix = format!(
618            "{}{}",
619            self.service_name,
620            if reply { REPLY_SUFFIX } else { REQUEST_SUFFIX }
621        );
622        self.bases
623            .iter()
624            .map(|base| compose_topic_name(style, &base.qualified_name, &suffix))
625            .collect()
626    }
627
628    /// Every operation a binding emits: the declared ones, then the ones
629    /// DDS-RPC 1.0 §7.5.1.1.3 implies from the attributes.
630    ///
631    /// Rule 1: "Each attribute in an interface maps to a pair of IDL
632    /// operations `get_attribute_<attribute-name>` and
633    /// `set_attribute_<attribute-name>` in the same interface." The
634    /// order is declaration order of the operations followed by
635    /// declaration order of the attributes, which is what fixes the
636    /// discriminator values of the call union: a generator that sorted
637    /// them would renumber a service that had only gained an attribute.
638    #[must_use]
639    pub fn all_operations(&self) -> Vec<Operation> {
640        let mut out = self.operations.clone();
641        for attribute in &self.attributes {
642            out.push(Operation {
643                node_id: Some(attribute.node_id),
644                name: format!("get_attribute_{}", attribute.name),
645                returns: Some(attribute.type_),
646                params: Vec::new(),
647                raises: attribute.get_raises.clone(),
648                origin: Origin::AttributeGetter,
649                mutable: false,
650            });
651            if attribute.readonly {
652                continue;
653            }
654            out.push(Operation {
655                node_id: Some(attribute.node_id),
656                name: format!("set_attribute_{}", attribute.name),
657                returns: None,
658                params: vec![Parameter {
659                    name: attribute.name.clone(),
660                    direction: ParamDirection::In,
661                    type_: attribute.type_,
662                }],
663                raises: attribute.set_raises.clone(),
664                origin: Origin::AttributeSetter,
665                mutable: false,
666            });
667        }
668        out
669    }
670}
671
672/// The request topic type's name. DDS-RPC 1.0 §7.5.1.1.6 rule 1: "The
673/// name of Request structure shall be `${interfaceName}_Request` in the
674/// same module as the interface" — the *simple* interface name, and not
675/// the topic name, which §7.4.1 builds differently.
676#[must_use]
677pub fn request_type_name(service: &str) -> String {
678    format!("{service}{REQUEST_SUFFIX}")
679}
680
681/// The reply topic type's name. DDS-RPC 1.0 §7.5.1.1.7.
682#[must_use]
683pub fn reply_type_name(service: &str) -> String {
684    format!("{service}{REPLY_SUFFIX}")
685}
686
687/// Which language binding a topic name is being built for. DDS-RPC 1.0
688/// §7.4.1, last paragraph: "For the request-reply style language
689/// binding, `<interface_name>` and the following underscore shall not be
690/// captured automatically."
691///
692/// The same value `crates/rpc/src/topic_naming.rs::BindingStyle` carries
693/// for the run time. Two enums and not one because this crate must not
694/// depend on `zerodds-rpc` — that edge is a cycle, `zerodds-rpc` →
695/// `zerodds-dcps` → `zerodds-lang-rules` — and
696/// `crates/rpc/tests/language_binding_agreement.rs` holds the names the
697/// two produce against each other rather than trusting the duplication.
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
699pub enum BindingStyle {
700    /// §7.11.1.1.2 — the interface segment is captured.
701    #[default]
702    FunctionCall,
703    /// §7.11.1.1.1 — it is not.
704    RequestReply,
705}
706
707impl BindingStyle {
708    /// Both styles.
709    pub const ALL: [Self; 2] = [Self::FunctionCall, Self::RequestReply];
710
711    /// Whether this style prefixes `<interface_name>` and an underscore.
712    #[must_use]
713    pub const fn captures_interface_name(self) -> bool {
714        matches!(self, Self::FunctionCall)
715    }
716}
717
718/// The `<interface_name>` of §7.4.1 from a module chain and a simple
719/// name: the module names joined by underscores, then the interface.
720#[must_use]
721pub fn qualified_interface_name(modules: &[&str], interface: &str) -> String {
722    let mut out = String::new();
723    for module in modules {
724        if module.is_empty() {
725            continue;
726        }
727        out.push_str(module);
728        out.push('_');
729    }
730    out.push_str(interface);
731    out
732}
733
734/// One topic name of §7.4.1, from the interface segment and the tail
735/// after it.
736#[must_use]
737pub fn compose_topic_name(style: BindingStyle, qualified_interface: &str, suffix: &str) -> String {
738    if style.captures_interface_name() {
739        format!("{qualified_interface}_{suffix}")
740    } else {
741        suffix.to_string()
742    }
743}
744
745/// The request union's name. DDS-RPC 1.0 §7.5.1.1.4.
746#[must_use]
747pub fn call_union_name(service: &str) -> String {
748    format!("{service}_Call")
749}
750
751/// The reply union's name. DDS-RPC 1.0 §7.5.1.1.7 rule 1: "The Return
752/// union name shall be `${interfaceName}_Return`."
753///
754/// Not `${I}_Result` — that name belongs to the *per-operation* union of
755/// §7.5.1.1.5, and one name over two shapes is how a reader ends up
756/// decoding the wrong one.
757#[must_use]
758pub fn return_union_name(service: &str) -> String {
759    format!("{service}_Return")
760}
761
762/// The per-operation result union's name. DDS-RPC 1.0 §7.5.1.1.5 rule
763/// 1.
764#[must_use]
765pub fn result_union_name(service: &str, operation: &str) -> String {
766    format!("{service}_{operation}_Result")
767}
768
769/// The constant §7.5.1.1.6 rule 4 requires beside a `Call` union:
770/// `${interfaceName}_${operationName}_Hash`.
771#[must_use]
772pub fn operation_hash_constant(service: &str, operation: &str) -> String {
773    format!("{service}_{operation}_Hash")
774}
775
776/// The constant §7.5.1.1.5 rule 4a requires beside a `Result` union:
777/// `${exceptionType}_Ex_Hash`.
778#[must_use]
779pub fn exception_hash_constant(exception: &str) -> String {
780    format!("{exception}_Ex_Hash")
781}
782
783/// The member name §7.5.1.1.5 rule 5b gives an exception case: "the
784/// lower-case version of the exception name with the suffix `_ex`
785/// added".
786#[must_use]
787pub fn exception_member_name(exception: &str) -> String {
788    format!("{}_ex", exception.to_lowercase())
789}
790
791/// The per-operation request struct's name. DDS-RPC 1.0 §7.5.1.1.4.
792#[must_use]
793pub fn in_struct_name(service: &str, operation: &str) -> String {
794    format!("{service}_{operation}_In")
795}
796
797/// The per-operation reply struct's name.
798///
799/// DDS-RPC 1.0 §7.5.1.1.5 names it as the reply case type, and
800/// §7.11.1.1.2 rule 7.i names the same type as what an asynchronous
801/// operation with `out` or `inout` parameters returns a `future` of.
802#[must_use]
803pub fn out_struct_name(service: &str, operation: &str) -> String {
804    format!("{service}_{operation}_Out")
805}
806
807/// The discriminator label a `Call` or `Return` union gives an
808/// operation. DDS-RPC 1.0 §7.5.1.1.6 rule 5a: "The integral value of the
809/// case label shall be the HASH of `${operationName}`" — the unqualified
810/// name, through the algorithm of §7.5.1.1.2.
811///
812/// The same 32 bits `crates/rpc/src/rpc_hash.rs::rpc_member_hash`
813/// produces, read as the `long` the union's discriminator is.
814/// `crates/rpc/tests/language_binding_agreement.rs` holds the two
815/// against each other.
816#[must_use]
817pub fn operation_hash(operation: &str) -> i32 {
818    // MD5 of the name, first four octets, little-endian — §7.5.1.1.2's
819    // pseudo-implementation reads them as
820    // `md5[0] + 256*md5[1] + 256*256*md5[2] + 256*256*256*md5[3]`.
821    let digest = zerodds_types::hash::hash_bytes(operation.as_bytes());
822    u32::from_le_bytes([digest.0[0], digest.0[1], digest.0[2], digest.0[3]]) as i32
823}
824
825/// The HASH of §7.5.1.1.5 rule 4b: the exception's *fully qualified*
826/// name, `::`-separated.
827#[must_use]
828pub fn exception_hash(scoped_name: &str) -> i32 {
829    operation_hash(scoped_name)
830}
831
832/// How deep a module chain is followed before the model is taken to be
833/// malformed. IDL nests nowhere near this.
834pub const MAX_MODULE_DEPTH: usize = 64;
835
836/// The modules a declaration nests in, outermost first, with the handle
837/// and the IDL name of each.
838///
839/// The root module is the anonymous global scope and contributes no
840/// entry. Every column already has this walk — `adapter::cpp`,
841/// `adapter::java` and `adapter::typescript` each keep a private copy,
842/// each escaping to its own alphabet — and a fourth copy escaping to a
843/// fourth would be a fourth answer to the question of where a
844/// declaration goes. This one escapes to none: the caller spells the
845/// names, because that is the only part of the answer that is the
846/// language's.
847///
848/// # Errors
849/// [`InterfaceError::NoNode`] when the model does not answer for a
850/// module in the chain, or the chain runs past [`MAX_MODULE_DEPTH`].
851pub fn module_chain(
852    ast: &Ast,
853    module: Id<ModuleNode>,
854) -> Result<Vec<(Id<ModuleNode>, String)>, InterfaceError> {
855    let mut out = Vec::new();
856    let mut current = Some(module);
857    let mut depth = 0usize;
858    while let Some(id) = current {
859        if depth > MAX_MODULE_DEPTH {
860            return Err(InterfaceError::NoNode {
861                kind: "module",
862                at: format!("the parent chain runs past {MAX_MODULE_DEPTH}"),
863            });
864        }
865        depth += 1;
866        let node = ast.module(id).map_err(|_| InterfaceError::NoNode {
867            kind: "module",
868            at: "the enclosing module".to_string(),
869        })?;
870        if !node.name.is_empty() {
871            out.push((id, node.name.clone()));
872        }
873        current = node.parent;
874    }
875    out.reverse();
876    Ok(out)
877}
878
879/// Every interface of the model, in the order the model iterates them.
880///
881/// # Errors
882/// [`InterfaceError`], naming the interface and what about it did not
883/// reach the seam — never a silently dropped operation, parameter or
884/// attribute.
885pub fn services(ast: &Ast) -> Result<Vec<Service>, InterfaceError> {
886    let mut out = Vec::new();
887    for (id, _) in ast.interfaces() {
888        out.push(service(ast, id)?);
889    }
890    Ok(out)
891}
892
893/// One interface of the model.
894///
895/// # Errors
896/// As [`services`].
897pub fn service(ast: &Ast, id: Id<InterfaceNode>) -> Result<Service, InterfaceError> {
898    let node = ast.interface(id).map_err(|_| InterfaceError::NoNode {
899        kind: "interface",
900        at: "the model".to_string(),
901    })?;
902    if node.completeness != zerodds_ast::Completeness::Complete {
903        return Err(InterfaceError::ForwardWithoutDefinition {
904            name: node.name.clone(),
905        });
906    }
907
908    reject_outside_service_grammar(ast, node)?;
909
910    let mut bases = Vec::with_capacity(node.bases.len());
911    for base in &node.bases {
912        let base_id = base.resolved().ok_or_else(|| InterfaceError::Unresolved {
913            construct: "base interface",
914            at: node.name.clone(),
915        })?;
916        let base_node = ast.interface(base_id).map_err(|_| InterfaceError::NoNode {
917            kind: "base interface",
918            at: node.name.clone(),
919        })?;
920        bases.push(Base {
921            id: base_id,
922            name: base_node.name.clone(),
923            qualified_name: qualified_name_of(ast, base_node)?,
924        });
925    }
926
927    let mut operations = Vec::with_capacity(node.operations.len());
928    for operation in &node.operations {
929        let op = ast
930            .operation(*operation)
931            .map_err(|_| InterfaceError::NoNode {
932                kind: "operation",
933                at: node.name.clone(),
934            })?;
935        operations.push(one_operation(ast, &node.name, op)?);
936    }
937
938    let mut attributes = Vec::with_capacity(node.attributes.len());
939    for attribute in &node.attributes {
940        let attr = ast
941            .attribute(*attribute)
942            .map_err(|_| InterfaceError::NoNode {
943                kind: "attribute",
944                at: node.name.clone(),
945            })?;
946        attributes.push(one_attribute(ast, &node.name, attr)?);
947    }
948
949    let service = Service {
950        id,
951        node_id: node.node_id,
952        qualified_name: qualified_name_of(ast, node)?,
953        service_name: service_annotation_name(ast, &node.facets)
954            .unwrap_or_else(|| DEFAULT_SERVICE_NAME.to_string()),
955        request_topic: annotation_string(ast, &node.facets, "DDSRequestTopic", "name"),
956        reply_topic: annotation_string(ast, &node.facets, "DDSReplyTopic", "name"),
957        qos_request_profile: annotation_named_string(ast, &node.facets, "Qos", "RequestProfile"),
958        qos_reply_profile: annotation_named_string(ast, &node.facets, "Qos", "ReplyProfile"),
959        name: node.name.clone(),
960        module: node.module,
961        kind: node.kind,
962        bases,
963        operations,
964        attributes,
965        mapping: ServiceMapping::Basic,
966    };
967
968    // §7.5.1.1.3 rule 1, and the plain duplicate: the declared
969    // operations and the ones the attributes imply share one namespace.
970    let every = service.all_operations();
971    for (index, operation) in every.iter().enumerate() {
972        if every[..index]
973            .iter()
974            .any(|other| other.name == operation.name)
975        {
976            return Err(InterfaceError::DuplicateOperation {
977                interface: service.name.clone(),
978                name: operation.name.clone(),
979            });
980        }
981    }
982    Ok(service)
983}
984
985/// DDS-RPC 1.0 §7.3.1.1: what the service grammar removed from IDL 4.2.
986/// The same list `zerodds_rpc::service_mapping::lower_service` refuses;
987/// `zerodds-rpc` cannot be a dependency of this crate (it would be a
988/// cycle), so the check is written here against the same model.
989fn reject_outside_service_grammar(ast: &Ast, iface: &InterfaceNode) -> Result<(), InterfaceError> {
990    let refuse = |construct: &'static str, at: String| {
991        Err(InterfaceError::OutsideServiceGrammar { construct, at })
992    };
993    // Production (7): only `interface` is in `<interface_header>`.
994    if iface.kind != InterfaceKind::Interface {
995        return refuse(
996            "an interface declaration of a form other than `interface`",
997            format!("{} {}", iface.kind.as_str(), iface.name),
998        );
999    }
1000    // Productions (1) and (2): file-scoped directives are kept on the
1001    // root module because they leave no node of their own.
1002    if let Ok(module) = ast.module(ast.root_module()) {
1003        if let Some(corba) = module.facets.corba.as_ref() {
1004            if let Some(first) = corba.imports.first() {
1005                return refuse("an `import` directive", first.clone());
1006            }
1007            if let Some(first) = corba.repository_directives.first() {
1008                return refuse("a `typeid`/`typeprefix` directive", first.clone());
1009            }
1010        }
1011    }
1012    // Production (9): an interface body holds attributes and operations
1013    // only. A nested declaration is lowered into the model all the same,
1014    // so it is found by its declaration scope.
1015    let nested = |module: Id<ModuleNode>, scope: Option<&str>| {
1016        module == iface.module && scope == Some(iface.name.as_str())
1017    };
1018    for (_, node) in ast.types() {
1019        if nested(node.module, node.scope.as_deref()) {
1020            return refuse(
1021                "a type declaration in an interface body",
1022                format!("{}::{}", iface.name, node.name),
1023            );
1024        }
1025    }
1026    for (_, node) in ast.constants() {
1027        if nested(node.module, node.scope.as_deref()) {
1028            return refuse(
1029                "a constant declaration in an interface body",
1030                format!("{}::{}", iface.name, node.name),
1031            );
1032        }
1033    }
1034    for (_, node) in ast.exceptions() {
1035        if nested(node.module, node.scope.as_deref()) {
1036            return refuse(
1037                "an exception declaration in an interface body",
1038                format!("{}::{}", iface.name, node.name),
1039            );
1040        }
1041    }
1042    Ok(())
1043}
1044
1045/// Same bound `Ast::all_operations_of`'s own walk uses, so a cycle this
1046/// crate refuses is refused at the same depth the frontend already
1047/// refuses it at.
1048const MAX_INHERITANCE_DEPTH: usize = 64;
1049
1050/// Every operation a *flattened* interface binding writes:
1051/// [`Service::all_operations`] — this interface's own declared
1052/// operations, plus the ones DDS-RPC 1.0 §7.5.1.1.3 implies from its
1053/// attributes — preceded by the same from every base, recursively, in
1054/// the order [`Ast::all_operations_of`] already walks bases: depth- and
1055/// cycle-bounded by `MAX_INHERITANCE_DEPTH`, and deduplicated by node
1056/// identity where a diamond makes one operation or attribute reachable
1057/// through more than one path.
1058///
1059/// OMG IDL 4.2 §7.4.3: a derived interface "inherits all the operations
1060/// and attributes of its base interfaces" — `Ast::all_operations_of`
1061/// gives the first half; this is both, for the language bindings with
1062/// no native multiple-supertrait mechanism to compose them through
1063/// separately instead. `emit::interface_rust` has one and reaches for
1064/// it rather than this.
1065///
1066/// # Errors
1067/// As [`service`], for any interface reached along the chain.
1068pub fn all_operations_flattened(
1069    ast: &Ast,
1070    interface: Id<InterfaceNode>,
1071) -> Result<Vec<Operation>, InterfaceError> {
1072    let mut seen_interfaces = Vec::new();
1073    let mut seen_ops = Vec::new();
1074    let mut out = Vec::new();
1075    collect_flattened_operations(
1076        ast,
1077        interface,
1078        &mut seen_interfaces,
1079        &mut seen_ops,
1080        &mut out,
1081        0,
1082    )?;
1083    Ok(out)
1084}
1085
1086/// Every attribute a *flattened* interface binding writes as a native
1087/// property (C#, Swift, …, rather than a `get_`/`set_` method pair):
1088/// this interface's own attributes, preceded by every base's, in the
1089/// same order and under the same bound as
1090/// [`all_operations_flattened`].
1091///
1092/// A binding that renders these as properties calls this instead of
1093/// [`all_operations_flattened`] for the attribute half and filters that
1094/// one to [`Origin::Declared`] for the operation half — the two
1095/// together are §7.4.3's "all the operations and attributes", once
1096/// each, never both ways for the same attribute.
1097///
1098/// # Errors
1099/// As [`service`], for any interface reached along the chain.
1100pub fn all_attributes_flattened(
1101    ast: &Ast,
1102    interface: Id<InterfaceNode>,
1103) -> Result<Vec<Attribute>, InterfaceError> {
1104    let mut seen_interfaces = Vec::new();
1105    let mut seen_attrs = Vec::new();
1106    let mut out = Vec::new();
1107    collect_flattened_attributes(
1108        ast,
1109        interface,
1110        &mut seen_interfaces,
1111        &mut seen_attrs,
1112        &mut out,
1113        0,
1114    )?;
1115    Ok(out)
1116}
1117
1118/// zerodds-lint: recursion-depth 64 (one level per base interface;
1119/// `depth` refuses a chain at `MAX_INHERITANCE_DEPTH`, and
1120/// `seen_interfaces` admits each interface once, so a cycle stops on the
1121/// second visit)
1122fn collect_flattened_attributes(
1123    ast: &Ast,
1124    interface: Id<InterfaceNode>,
1125    seen_interfaces: &mut Vec<Id<InterfaceNode>>,
1126    seen_attrs: &mut Vec<NodeId>,
1127    out: &mut Vec<Attribute>,
1128    depth: usize,
1129) -> Result<(), InterfaceError> {
1130    // A base reached twice (a diamond) is already in `out`; a chain past
1131    // the limit is refused rather than cut.
1132    if seen_interfaces.contains(&interface) {
1133        return Ok(());
1134    }
1135    let this = service(ast, interface)?;
1136    if depth >= MAX_INHERITANCE_DEPTH {
1137        return Err(InterfaceError::TooDeep {
1138            name: this.name.clone(),
1139            limit: MAX_INHERITANCE_DEPTH,
1140        });
1141    }
1142    seen_interfaces.push(interface);
1143    for base in &this.bases {
1144        collect_flattened_attributes(ast, base.id, seen_interfaces, seen_attrs, out, depth + 1)?;
1145    }
1146    for attribute in this.attributes {
1147        if seen_attrs.contains(&attribute.node_id) {
1148            continue;
1149        }
1150        seen_attrs.push(attribute.node_id);
1151        out.push(attribute);
1152    }
1153    Ok(())
1154}
1155
1156/// zerodds-lint: recursion-depth 64 (one level per base interface;
1157/// `depth` refuses a chain at `MAX_INHERITANCE_DEPTH`, and
1158/// `seen_interfaces` admits each interface once, so a cycle stops on the
1159/// second visit)
1160fn collect_flattened_operations(
1161    ast: &Ast,
1162    interface: Id<InterfaceNode>,
1163    seen_interfaces: &mut Vec<Id<InterfaceNode>>,
1164    seen_ops: &mut Vec<(NodeId, String)>,
1165    out: &mut Vec<Operation>,
1166    depth: usize,
1167) -> Result<(), InterfaceError> {
1168    // A base reached twice (a diamond) is already in `out`; a chain past
1169    // the limit is refused rather than cut.
1170    if seen_interfaces.contains(&interface) {
1171        return Ok(());
1172    }
1173    let this = service(ast, interface)?;
1174    if depth >= MAX_INHERITANCE_DEPTH {
1175        return Err(InterfaceError::TooDeep {
1176            name: this.name.clone(),
1177            limit: MAX_INHERITANCE_DEPTH,
1178        });
1179    }
1180    seen_interfaces.push(interface);
1181    for base in &this.bases {
1182        collect_flattened_operations(ast, base.id, seen_interfaces, seen_ops, out, depth + 1)?;
1183    }
1184    for operation in this.all_operations() {
1185        if let Some(id) = operation.node_id {
1186            // `(node_id, name)`, not `node_id` alone: an attribute's
1187            // getter and setter (`Service::all_operations`) share one
1188            // `node_id` — both point back at the one `attribute`
1189            // declaration that implies them — so `node_id` alone would
1190            // make the setter look like a diamond-repeat of the getter
1191            // and drop it. A diamond repeat (the same operation reached
1192            // through two base paths) shares both the `node_id` *and*
1193            // the name; a getter/setter pair shares only the former.
1194            let key = (id, operation.name.clone());
1195            if seen_ops.contains(&key) {
1196                continue;
1197            }
1198            seen_ops.push(key);
1199        }
1200        out.push(operation);
1201    }
1202    Ok(())
1203}
1204
1205// -- the Enhanced Service Mapping -------------------------------------------
1206
1207/// Which of the two service mappings of DDS-RPC 1.0 §7.5.1 a generator
1208/// writes the topic types of.
1209///
1210/// The Basic mapping is the default: §7.5.1.1 is the one every DDS
1211/// implementation can carry, and the Enhanced one (§7.5.1.2) needs
1212/// XTypes. An interface does not say which of the two it is for — the
1213/// same IDL has both mappings — so the choice is the generator's.
1214#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1215pub enum ServiceMapping {
1216    /// §7.5.1.1: a `Request` structure with a header and a `Call`
1217    /// union, a `Reply` structure with a header and a `Return` union.
1218    #[default]
1219    Basic,
1220    /// §7.5.1.2: `@Choice @Autoid` `Request` and `Reply` structures
1221    /// over the operations, without a header. An operation with nothing to
1222    /// carry has the `dummy` member of §7.5.1.1.4 rule 3c, as the example
1223    /// of §7.5.1.2.6 shows ([`EmptyOperationStyle::DummyMember`]).
1224    Enhanced,
1225    /// [`Self::Enhanced`], with an operation that has nothing to carry as
1226    /// an empty structure marked `@Empty` (§7.5.1.2.1.3,
1227    /// [`EmptyOperationStyle::EmptyAnnotation`]): the other reading of the
1228    /// clause.
1229    EnhancedEmptyStructures,
1230}
1231
1232impl ServiceMapping {
1233    /// Whether this is one of the Enhanced readings.
1234    #[must_use]
1235    pub const fn is_enhanced(self) -> bool {
1236        matches!(self, Self::Enhanced | Self::EnhancedEmptyStructures)
1237    }
1238
1239    /// How an operation with nothing to carry is mapped.
1240    #[must_use]
1241    pub const fn empty_style(self) -> EmptyOperationStyle {
1242        match self {
1243            Self::EnhancedEmptyStructures => EmptyOperationStyle::EmptyAnnotation,
1244            Self::Basic | Self::Enhanced => EmptyOperationStyle::DummyMember,
1245        }
1246    }
1247}
1248
1249/// The member a `Reply` structure carries the remote exception code in.
1250/// §7.5.1.2.5 rule 5.
1251pub const REMOTE_EX_MEMBER: &str = "remoteEx";
1252
1253/// The mask that turns the 32-bit HASH of §7.5.1.1.2 into a member id,
1254/// which has 28 bits (XTypes 1.3 §7.4.3.4.2, EMHEADER).
1255pub const MEMBER_ID_MASK: u32 = 0x0FFF_FFFF;
1256
1257/// How an operation with nothing to carry is mapped in the Enhanced
1258/// Service Mapping.
1259///
1260/// §7.5.1.2.1.3 introduces `@Empty` for it, while §7.5.1.2.2 and
1261/// §7.5.1.2.3 take over the `dummy` member of §7.5.1.1.4 rule 3c and
1262/// the example of §7.5.1.2.6 shows it. The default is the `dummy`
1263/// member. The same setting `crates/rpc/src/codegen.rs` has.
1264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1265pub enum EmptyOperationStyle {
1266    /// One `dummy` member of type `UnusedMember`.
1267    #[default]
1268    DummyMember,
1269    /// An empty structure marked `@Empty`.
1270    EmptyAnnotation,
1271}
1272
1273/// The type of a member of an Enhanced structure.
1274#[derive(Debug, Clone, PartialEq, Eq)]
1275pub enum EnhancedMemberType {
1276    /// A parameter's or the return value's IDL type.
1277    Idl(Id<TypeNode>),
1278    /// `dds::rpc::UnusedMember`, the `dummy` member.
1279    Dummy,
1280    /// A structure of this mapping, by name.
1281    Struct(String),
1282    /// An exception the operation raises (§7.5.1.2.3 rule 6b).
1283    Exception(Raise),
1284    /// `dds::rpc::RemoteExceptionCode_t`, the type of `remoteEx`. The
1285    /// clause prints `dds::rpc::RemoteException_t`, which no clause
1286    /// defines.
1287    RemoteException,
1288}
1289
1290/// One member of an Enhanced structure.
1291#[derive(Debug, Clone, PartialEq, Eq)]
1292pub struct EnhancedMember {
1293    /// The member's name.
1294    pub name: String,
1295    /// Its type.
1296    pub type_: EnhancedMemberType,
1297    /// The member id `@Autoid` gives it: the HASH of the name
1298    /// (§7.5.1.2.1.2), 28 bits of it.
1299    pub member_id: u32,
1300}
1301
1302/// A structure of the Enhanced Service Mapping.
1303///
1304/// The same shape as `crates/rpc/src/codegen.rs::EnhancedStruct`;
1305/// `crates/rpc/tests/language_binding_agreement.rs` holds the two against
1306/// each other.
1307#[derive(Debug, Clone, PartialEq, Eq)]
1308pub struct EnhancedStruct {
1309    /// The structure's name.
1310    pub name: String,
1311    /// `@Choice` (§7.5.1.2.1.1).
1312    pub choice: bool,
1313    /// `@Autoid` (§7.5.1.2.1.2).
1314    pub autoid: bool,
1315    /// `@Empty` (§7.5.1.2.1.3).
1316    pub empty: bool,
1317    /// `@Extensibility(MUTABLE_EXTENSIBILITY)`.
1318    pub mutable: bool,
1319    /// The members, in the order the clause lists them.
1320    pub members: Vec<EnhancedMember>,
1321}
1322
1323/// One operation's three structures.
1324#[derive(Debug, Clone, PartialEq, Eq)]
1325pub struct EnhancedOperation {
1326    /// The simple name of the interface that declares the operation,
1327    /// which the structure names are built from: an inherited operation
1328    /// keeps its base's name (§7.5.1.2.6).
1329    pub interface: String,
1330    /// The module that interface is declared in, where its structures
1331    /// are.
1332    pub module: Id<ModuleNode>,
1333    /// The operation.
1334    pub operation: Operation,
1335    /// `${I}_${op}_In` (§7.5.1.2.2).
1336    pub in_struct: EnhancedStruct,
1337    /// `${I}_${op}_Out` (§7.5.1.2.3 rule 1).
1338    pub out_struct: EnhancedStruct,
1339    /// `${I}_${op}_Result` (§7.5.1.2.3 rules 2 to 7).
1340    pub result_struct: EnhancedStruct,
1341}
1342
1343/// An interface under the Enhanced Service Mapping.
1344#[derive(Debug, Clone, PartialEq, Eq)]
1345pub struct EnhancedService {
1346    /// Every operation: the inherited ones, base by base, and then the
1347    /// interface's own (the order of the example of §7.5.1.2.6).
1348    pub operations: Vec<EnhancedOperation>,
1349    /// `${interfaceName}_Request` (§7.5.1.2.4).
1350    pub request: EnhancedStruct,
1351    /// `${interfaceName}_Reply` (§7.5.1.2.5).
1352    pub reply: EnhancedStruct,
1353}
1354
1355/// An operation with the interface that declares it and that interface's
1356/// module.
1357pub type DeclaredOperation = (String, Id<ModuleNode>, Operation);
1358
1359/// The member id `@Autoid` gives a member of this name.
1360#[must_use]
1361pub fn member_id(name: &str) -> u32 {
1362    (operation_hash(name) as u32) & MEMBER_ID_MASK
1363}
1364
1365/// The operations of `service` and of every base, each with the simple
1366/// name of the interface that declares it and the module it is in.
1367///
1368/// The bases in declaration order, each base's own bases before the base
1369/// and the service's own operations last — the order of the example of
1370/// §7.5.1.2.6, in which `Calculator_Request` lists `add` and `sub` of
1371/// its bases before `on` and `off` — a base reached twice listed once.
1372///
1373/// # Errors
1374/// As [`service`], for any interface reached along the chain.
1375pub fn operations_with_interface_flattened(
1376    ast: &Ast,
1377    service: &Service,
1378) -> Result<Vec<DeclaredOperation>, InterfaceError> {
1379    let mut out: Vec<DeclaredOperation> = Vec::new();
1380    let mut seen: Vec<Id<InterfaceNode>> = Vec::new();
1381    for base in &service.bases {
1382        collect_inherited(ast, base.id, &mut seen, &mut out, 0)?;
1383    }
1384    out.extend(
1385        service
1386            .all_operations()
1387            .into_iter()
1388            .map(|operation| (service.name.clone(), service.module, operation)),
1389    );
1390    Ok(out)
1391}
1392
1393/// zerodds-lint: recursion-depth 64 (one level per base interface;
1394/// `depth` refuses a chain at `MAX_INHERITANCE_DEPTH`, and `seen`
1395/// admits each interface once)
1396fn collect_inherited(
1397    ast: &Ast,
1398    interface: Id<InterfaceNode>,
1399    seen: &mut Vec<Id<InterfaceNode>>,
1400    out: &mut Vec<DeclaredOperation>,
1401    depth: usize,
1402) -> Result<(), InterfaceError> {
1403    if seen.contains(&interface) {
1404        return Ok(());
1405    }
1406    let this = service(ast, interface)?;
1407    if depth >= MAX_INHERITANCE_DEPTH {
1408        return Err(InterfaceError::TooDeep {
1409            name: this.name.clone(),
1410            limit: MAX_INHERITANCE_DEPTH,
1411        });
1412    }
1413    seen.push(interface);
1414    for base in &this.bases {
1415        collect_inherited(ast, base.id, seen, out, depth + 1)?;
1416    }
1417    for operation in this.all_operations() {
1418        out.push((this.name.clone(), this.module, operation));
1419    }
1420    Ok(())
1421}
1422
1423/// `@Autoid` structure over `members`; a hash collision is an error
1424/// (§7.5.1.2.1.2).
1425fn autoid_struct(
1426    name: String,
1427    choice: bool,
1428    mutable: bool,
1429    empty: bool,
1430    members: Vec<(String, EnhancedMemberType)>,
1431) -> Result<EnhancedStruct, BindingError> {
1432    let mut built: Vec<EnhancedMember> = Vec::with_capacity(members.len());
1433    for (member_name, type_) in members {
1434        let id = member_id(&member_name);
1435        if let Some(clash) = built.iter().find(|member| member.member_id == id) {
1436            return Err(BindingError::Codec(format!(
1437                "members `{}` and `{member_name}` of `{name}` hash to the same member id {id:#010x}",
1438                clash.name
1439            )));
1440        }
1441        built.push(EnhancedMember {
1442            name: member_name,
1443            type_,
1444            member_id: id,
1445        });
1446    }
1447    Ok(EnhancedStruct {
1448        name,
1449        choice,
1450        autoid: true,
1451        empty,
1452        mutable,
1453        members: built,
1454    })
1455}
1456
1457/// The name of the return value's member in an `Out` structure.
1458/// §7.5.1.1.5 rule 3b: `return_`, or `return_N` where an `out` parameter
1459/// has that name.
1460fn return_member_name(taken: &[String]) -> String {
1461    if !taken.iter().any(|name| name == RETURN_MEMBER) {
1462        return RETURN_MEMBER.to_string();
1463    }
1464    for n in 1..=i32::MAX {
1465        let candidate = format!("{RETURN_MEMBER}{n}");
1466        if !taken.contains(&candidate) {
1467            return candidate;
1468        }
1469    }
1470    format!("{RETURN_MEMBER} ")
1471}
1472
1473/// `remoteEx`, or `remoteEx_N` where an operation has that name
1474/// (§7.5.1.2.5 rule 5).
1475fn remote_ex_member_name(operations: &[EnhancedOperation]) -> String {
1476    let taken = |candidate: &str| {
1477        operations
1478            .iter()
1479            .any(|operation| operation.operation.name == candidate)
1480    };
1481    if !taken(REMOTE_EX_MEMBER) {
1482        return REMOTE_EX_MEMBER.to_string();
1483    }
1484    for n in 1..=i32::MAX {
1485        let candidate = format!("{REMOTE_EX_MEMBER}_{n}");
1486        if !taken(&candidate) {
1487            return candidate;
1488        }
1489    }
1490    format!("{REMOTE_EX_MEMBER} ")
1491}
1492
1493/// The Enhanced Service Mapping of `service` (DDS-RPC 1.0 §7.5.1.2).
1494///
1495/// # Errors
1496/// [`BindingError::Interface`] as [`service`]; [`BindingError::Codec`]
1497/// for two operations of one name, or two members of an `@Autoid`
1498/// structure that hash to one member id.
1499pub fn enhanced_service(
1500    ast: &Ast,
1501    service: &Service,
1502    empty: EmptyOperationStyle,
1503) -> Result<EnhancedService, BindingError> {
1504    let empty_annotation = empty == EmptyOperationStyle::EmptyAnnotation;
1505    let mut operations: Vec<EnhancedOperation> = Vec::new();
1506    for (interface, module, operation) in operations_with_interface_flattened(ast, service)? {
1507        if operations
1508            .iter()
1509            .any(|seen| seen.operation.name == operation.name)
1510        {
1511            return Err(BindingError::Codec(format!(
1512                "operation `{}` is declared twice in `{}` and its bases",
1513                operation.name, service.name
1514            )));
1515        }
1516        // §7.5.1.2.2: the rules of §7.5.1.1.4 rule 3.
1517        let mut in_members: Vec<(String, EnhancedMemberType)> = operation
1518            .inputs()
1519            .map(|param| (param.name.clone(), EnhancedMemberType::Idl(param.type_)))
1520            .collect();
1521        let in_empty = in_members.is_empty();
1522        if in_empty && !empty_annotation {
1523            in_members.push((DUMMY_MEMBER.to_string(), EnhancedMemberType::Dummy));
1524        }
1525        // §7.5.1.2.3 rule 1: the rules of §7.5.1.1.5 rule 3.
1526        let mut out_members: Vec<(String, EnhancedMemberType)> = operation
1527            .outputs()
1528            .map(|param| (param.name.clone(), EnhancedMemberType::Idl(param.type_)))
1529            .collect();
1530        if let Some(returns) = operation.returns {
1531            let taken: Vec<String> = out_members.iter().map(|(name, _)| name.clone()).collect();
1532            out_members.push((return_member_name(&taken), EnhancedMemberType::Idl(returns)));
1533        }
1534        let out_empty = out_members.is_empty();
1535        if out_empty && !empty_annotation {
1536            out_members.push((DUMMY_MEMBER.to_string(), EnhancedMemberType::Dummy));
1537        }
1538        let in_struct = autoid_struct(
1539            in_struct_name(&interface, &operation.name),
1540            false,
1541            operation.mutable,
1542            in_empty && empty_annotation,
1543            in_members,
1544        )?;
1545        let out_struct = autoid_struct(
1546            out_struct_name(&interface, &operation.name),
1547            false,
1548            operation.mutable,
1549            out_empty && empty_annotation,
1550            out_members,
1551        )?;
1552        // §7.5.1.2.3 rules 2 to 7.
1553        let mut result_members = vec![(
1554            RESULT_MEMBER.to_string(),
1555            EnhancedMemberType::Struct(out_struct.name.clone()),
1556        )];
1557        for raised in &operation.raises {
1558            result_members.push((
1559                exception_member_name(&raised.name),
1560                EnhancedMemberType::Exception(raised.clone()),
1561            ));
1562        }
1563        let result_struct = autoid_struct(
1564            result_union_name(&interface, &operation.name),
1565            true,
1566            operation.mutable,
1567            false,
1568            result_members,
1569        )?;
1570        operations.push(EnhancedOperation {
1571            interface,
1572            module,
1573            operation,
1574            in_struct,
1575            out_struct,
1576            result_struct,
1577        });
1578    }
1579
1580    // §7.5.1.2.4: a member per operation, typed `…_In`.
1581    let request = autoid_struct(
1582        request_type_name(&service.name),
1583        true,
1584        false,
1585        false,
1586        operations
1587            .iter()
1588            .map(|operation| {
1589                (
1590                    operation.operation.name.clone(),
1591                    EnhancedMemberType::Struct(operation.in_struct.name.clone()),
1592                )
1593            })
1594            .collect(),
1595    )?;
1596    // §7.5.1.2.5: a member per operation, typed `…_Result`, and `remoteEx`.
1597    let mut reply_members: Vec<(String, EnhancedMemberType)> = operations
1598        .iter()
1599        .map(|operation| {
1600            (
1601                operation.operation.name.clone(),
1602                EnhancedMemberType::Struct(operation.result_struct.name.clone()),
1603            )
1604        })
1605        .collect();
1606    reply_members.push((
1607        remote_ex_member_name(&operations),
1608        EnhancedMemberType::RemoteException,
1609    ));
1610    let reply = autoid_struct(
1611        reply_type_name(&service.name),
1612        true,
1613        false,
1614        false,
1615        reply_members,
1616    )?;
1617    Ok(EnhancedService {
1618        operations,
1619        request,
1620        reply,
1621    })
1622}
1623
1624/// The `<interface_name>` of §7.4.1 for one interface node.
1625fn qualified_name_of(ast: &Ast, node: &InterfaceNode) -> Result<String, InterfaceError> {
1626    let chain = module_chain(ast, node.module)?;
1627    let segments: Vec<&str> = chain.iter().map(|(_, name)| name.as_str()).collect();
1628    Ok(qualified_interface_name(&segments, &node.name))
1629}
1630
1631/// The `name = "…"` of `@service`, wherever the frontend put it.
1632///
1633/// The IDL frontend folds `@service` into the CORBA facet — IDL 4.2
1634/// §8.3.4 declares it there — rather than leaving it among the applied
1635/// annotations, so a reader that looked only at `facets.annotations`
1636/// finds nothing and every service silently takes the `"Service"`
1637/// default. `crates/rpc/src/annotations.rs::lower_rpc_annotations` reads
1638/// both places for the same reason.
1639///
1640/// Only the explicit `name = "…"` form is read: `@service("DDS")` names
1641/// a *platform*, and treating that as a service name would rename every
1642/// service that declares one.
1643fn service_annotation_name(ast: &Ast, facets: &zerodds_ast::Facets) -> Option<String> {
1644    if let Some(corba) = facets.corba.as_ref()
1645        && corba.is_service
1646        && let Some(name) = named_string_argument(ast, &corba.service_arguments, "name")
1647    {
1648        return Some(name);
1649    }
1650    let applied = applied_annotation(facets, "service")?;
1651    named_string_argument(ast, &applied.arguments, "name")
1652}
1653
1654/// The applied annotation with this simple name, if the facets carry
1655/// one.
1656///
1657/// A leading underscore comes off: IDL 4.2 §7.2.3.2 makes it "a purely
1658/// lexical convention that ONLY turns off keyword checking".
1659fn applied_annotation<'a>(
1660    facets: &'a zerodds_ast::Facets,
1661    annotation: &str,
1662) -> Option<&'a zerodds_ast::AppliedAnnotation> {
1663    facets.annotations.iter().find(|candidate| {
1664        let tail = candidate
1665            .name
1666            .rsplit("::")
1667            .next()
1668            .unwrap_or(&candidate.name);
1669        tail.strip_prefix('_').unwrap_or(tail) == annotation
1670    })
1671}
1672
1673/// The string of the argument written `key = "…"`.
1674fn named_string_argument(
1675    ast: &Ast,
1676    arguments: &[zerodds_ast::AppliedArgument],
1677    key: &str,
1678) -> Option<String> {
1679    let argument = arguments
1680        .iter()
1681        .find(|argument| argument.name.as_deref() == Some(key))?;
1682    literal_string(ast, argument.value)
1683}
1684
1685/// The string a constant expression evaluates to, if it is one.
1686fn literal_string(ast: &Ast, id: Id<zerodds_ast::ConstExprNode>) -> Option<String> {
1687    match &ast.expr(id).ok()?.expr {
1688        zerodds_ast::ConstExpr::Literal(zerodds_ast::Literal::String(text)) => Some(text.clone()),
1689        _ => None,
1690    }
1691}
1692
1693/// The string argument of one applied annotation, by the annotation's
1694/// simple name and the argument's key.
1695///
1696/// The generator's reader of the three annotations §7.4 gives it —
1697/// `@service(name = "…")`, `@DDSRequestTopic(name = "…")` and
1698/// `@DDSReplyTopic(name = "…")`. `crates/rpc/src/annotations.rs` reads
1699/// the same three for the run time and reads more besides; the two are
1700/// held against each other by
1701/// `crates/rpc/tests/language_binding_agreement.rs`, because two readers
1702/// of one annotation that disagree would put a client and a service on
1703/// two topics.
1704///
1705/// A leading underscore comes off the annotation's name: IDL 4.2
1706/// §7.2.3.2 makes it "a purely lexical convention that ONLY turns off
1707/// keyword checking".
1708fn annotation_string(
1709    ast: &Ast,
1710    facets: &zerodds_ast::Facets,
1711    annotation: &str,
1712    key: &str,
1713) -> Option<String> {
1714    let applied = applied_annotation(facets, annotation)?;
1715    // `@ann("x")` addresses the declaration's `value` member (IDL 4.2
1716    // §8.2); `@ann(key = "x")` names it. Both are read, the positional
1717    // one first, which is the order `crates/rpc/src/annotations.rs`
1718    // takes.
1719    let argument = applied
1720        .arguments
1721        .iter()
1722        .find(|argument| argument.name.is_none())
1723        .or_else(|| {
1724            applied
1725                .arguments
1726                .iter()
1727                .find(|argument| argument.name.as_deref() == Some(key))
1728        })?;
1729    literal_string(ast, argument.value)
1730}
1731
1732/// The string argument `key` of one applied annotation, by name only: an
1733/// annotation with several string attributes (`@Qos`) has no positional
1734/// one to take for any of them.
1735fn annotation_named_string(
1736    ast: &Ast,
1737    facets: &zerodds_ast::Facets,
1738    annotation: &str,
1739    key: &str,
1740) -> Option<String> {
1741    let applied = applied_annotation(facets, annotation)?;
1742    let argument = applied
1743        .arguments
1744        .iter()
1745        .find(|argument| argument.name.as_deref() == Some(key))?;
1746    literal_string(ast, argument.value)
1747}
1748
1749fn one_operation(
1750    ast: &Ast,
1751    interface: &str,
1752    op: &OperationNode,
1753) -> Result<Operation, InterfaceError> {
1754    let at = format!("{interface}::{}", op.name);
1755    // Production (87) removed `oneway` and `context`; §7.5.1.1.7 rule 5
1756    // gives every operation a Return-union case, so a `oneway` operation
1757    // would have a case for a reply it never sends.
1758    if op.oneway || applied_annotation(&op.facets, "oneway").is_some() {
1759        return Err(InterfaceError::OutsideServiceGrammar {
1760            construct: "a `oneway` operation",
1761            at,
1762        });
1763    }
1764    if !op.contexts.is_empty() {
1765        return Err(InterfaceError::OutsideServiceGrammar {
1766            construct: "a `context` clause",
1767            at,
1768        });
1769    }
1770    let returns = match &op.return_type {
1771        None => None,
1772        Some(reference) => {
1773            Some(
1774                reference
1775                    .resolved()
1776                    .ok_or_else(|| InterfaceError::Unresolved {
1777                        construct: "return type",
1778                        at: at.clone(),
1779                    })?,
1780            )
1781        }
1782    };
1783
1784    let mut params = Vec::with_capacity(op.params.len());
1785    for param in &op.params {
1786        let node = ast.param(*param).map_err(|_| InterfaceError::NoNode {
1787            kind: "parameter",
1788            at: at.clone(),
1789        })?;
1790        let type_ = node
1791            .type_
1792            .resolved()
1793            .ok_or_else(|| InterfaceError::Unresolved {
1794                construct: "parameter type",
1795                at: format!("{at}({})", node.name),
1796            })?;
1797        if params
1798            .iter()
1799            .any(|other: &Parameter| other.name == node.name)
1800        {
1801            return Err(InterfaceError::DuplicateParameter {
1802                at,
1803                name: node.name.clone(),
1804            });
1805        }
1806        params.push(Parameter {
1807            name: node.name.clone(),
1808            direction: node.direction,
1809            type_,
1810        });
1811    }
1812
1813    Ok(Operation {
1814        node_id: Some(op.node_id),
1815        name: op.name.clone(),
1816        returns,
1817        params,
1818        raises: raises(ast, &at, "raises", &op.raises)?,
1819        origin: Origin::Declared,
1820        mutable: applied_annotation(&op.facets, "Mutable").is_some(),
1821    })
1822}
1823
1824fn one_attribute(
1825    ast: &Ast,
1826    interface: &str,
1827    attr: &AttributeNode,
1828) -> Result<Attribute, InterfaceError> {
1829    let at = format!("{interface}::{}", attr.name);
1830    let type_ = attr
1831        .type_
1832        .resolved()
1833        .ok_or_else(|| InterfaceError::Unresolved {
1834            construct: "attribute type",
1835            at: at.clone(),
1836        })?;
1837    Ok(Attribute {
1838        node_id: attr.node_id,
1839        name: attr.name.clone(),
1840        type_,
1841        readonly: attr.readonly,
1842        get_raises: raises(ast, &at, "getraises", &attr.get_raises)?,
1843        set_raises: raises(ast, &at, "setraises", &attr.set_raises)?,
1844    })
1845}
1846
1847fn raises(
1848    ast: &Ast,
1849    at: &str,
1850    clause: &'static str,
1851    list: &[zerodds_ast_core::Ref<ExceptionNode>],
1852) -> Result<Vec<Raise>, InterfaceError> {
1853    let mut out = Vec::with_capacity(list.len());
1854    for reference in list {
1855        let id = reference.resolved().ok_or(InterfaceError::Unresolved {
1856            construct: clause,
1857            at: at.to_string(),
1858        })?;
1859        let node = ast.exception(id).map_err(|_| InterfaceError::NoNode {
1860            kind: "exception",
1861            at: at.to_string(),
1862        })?;
1863        let chain = module_chain(ast, node.module)?;
1864        let mut scoped: Vec<&str> = chain.iter().map(|(_, name)| name.as_str()).collect();
1865        scoped.push(&node.name);
1866        out.push(Raise {
1867            id,
1868            scoped_name: scoped.join("::"),
1869            name: node.name.clone(),
1870        });
1871    }
1872    Ok(out)
1873}