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}