Skip to main content

zerodds_ast/profile/
outside.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Declarations a profile has no data type for.
4//!
5//! DDS-XTypes 1.3 §7.3.1.1.1 admits IDL constructs outside the building
6//! blocks it names, with a rule for the compiler that does not support
7//! them: it "shall ignore them or issue a warning rather than halting
8//! with an error". `any`, `ValueBase`, `native` and an object reference
9//! have no type identifier and no wire form in DDS-XTypes, so a
10//! declaration that holds one — directly, through a member, an alias, a
11//! collection element or a base — cannot get a codec or a `TypeObject`.
12//!
13//! [`Ast::drop_outside_profile`] is the ignoring: it removes those
14//! declarations from the model and says which, so a code generator that
15//! walks the model never meets one and the rest of the file is generated.
16//! It does not decide for the caller: a front end that wants the model
17//! unchanged (a CORBA flow that does carry `any`) does not call it.
18
19use super::{Profile, Verdict, construct_verdict, verdict::ConstructKind};
20use crate::error::AstError;
21use crate::model::Ast;
22use crate::node::AnyId;
23use crate::node::type_::TypeKind;
24use alloc::collections::BTreeSet;
25use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use zerodds_ast_core::{Id, NodeId};
28
29/// A declaration [`Ast::drop_outside_profile`] removed.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Dropped {
32    /// The scoped name of the declaration, or its kind when it has none.
33    pub name: String,
34    /// The construct that made it unrepresentable, as the profile's
35    /// table spells it (`any`, `ValueBase`, `native`, ...).
36    pub construct: &'static str,
37    /// The paragraph that rules the construct out.
38    pub spec_reference: &'static str,
39    /// Why the profile has no type for the construct.
40    pub detail: &'static str,
41}
42
43/// The kinds that stop a data type from being planned: no wire form and
44/// no type identifier, so a declaration reaching one has neither a codec
45/// nor a `TypeObject`.
46const fn has_no_data_form(kind: &TypeKind) -> bool {
47    matches!(
48        kind,
49        TypeKind::Any | TypeKind::ValueBase | TypeKind::Native | TypeKind::ObjectRef { .. }
50    )
51}
52
53/// A node that is itself a construct the profile rules out, and what the
54/// profile says about it.
55struct Seed {
56    node: NodeId,
57    construct: ConstructKind,
58    spec_reference: &'static str,
59    detail: &'static str,
60}
61
62impl Ast {
63    /// Remove the data types that reach a construct `profile` has no type
64    /// for, and return what was removed.
65    ///
66    /// A type is removed when it is, or reaches through members, cases,
67    /// aliases, collection elements, map keys and values, bases or a
68    /// discriminator, a construct `construct_verdict(profile, …)`
69    /// rejects among `any`, `ValueBase`, `native` and an object
70    /// reference. The construct's own node stays in the model (it is the
71    /// shared node every `any` points at).
72    ///
73    /// A type that a constant, an operation, a parameter, an attribute,
74    /// an annotation member or a topic names is **kept**: removing it
75    /// would leave that declaration with a reference to nothing, and the
76    /// caller would then be told about a broken reference instead of the
77    /// construct. Such a type stays in the model and a code generator
78    /// reports the construct by name, as before.
79    ///
80    /// Nothing is removed under a profile that admits all four.
81    ///
82    /// # Errors
83    /// [`AstError`] when a removal breaks a structural rule of the model;
84    /// the types removed before it stay removed, so a caller treats the
85    /// model as spoilt.
86    pub fn drop_outside_profile(&mut self, profile: Profile) -> Result<Vec<Dropped>, AstError> {
87        // The nodes that are the offending construct, with what the
88        // profile says about them.
89        let mut seeds: Vec<Seed> = Vec::new();
90        for (_, node) in self.types() {
91            if !has_no_data_form(&node.kind) {
92                continue;
93            }
94            let construct = ConstructKind::of(&node.kind);
95            if let Verdict::Rejected {
96                spec_reference,
97                detail,
98                ..
99            } = construct_verdict(profile, construct)
100            {
101                seeds.push(Seed {
102                    node: node.node_id,
103                    construct,
104                    spec_reference,
105                    detail,
106                });
107            }
108        }
109        if seeds.is_empty() {
110            return Ok(Vec::new());
111        }
112
113        let named_elsewhere = self.types_named_outside_the_type_graph();
114        let mut doomed: Vec<(Id<crate::TypeNode>, NodeId, usize)> = Vec::new();
115        for (id, node) in self.types() {
116            if seeds.iter().any(|seed| seed.node == node.node_id) {
117                continue;
118            }
119            let reach = self.reachable_types(id);
120            if let Some(seed) = seeds.iter().position(|seed| reach.contains(seed.node)) {
121                if !named_elsewhere.contains(&node.node_id) {
122                    doomed.push((id, node.node_id, seed));
123                }
124            }
125        }
126
127        // The scoped names are read before anything is removed: a name
128        // cannot be asked of a node that is gone. An anonymous type — the
129        // `sequence<any>` behind a typedef — is removed with the
130        // declaration that holds it and is not reported on its own.
131        let names: Vec<Option<String>> = doomed
132            .iter()
133            .map(|(id, node, _)| {
134                if self.types.get(*id).map_or(true, |t| t.name.is_empty()) {
135                    return None;
136                }
137                self.scoped_name_of(AnyId::Type(*id, *node))
138                    .map(|scoped| scoped.to_string())
139            })
140            .collect();
141        let mut dropped = Vec::new();
142        for ((id, node, seed_at), name) in doomed.into_iter().zip(names) {
143            let seed = &seeds[seed_at];
144            self.remove(AnyId::Type(id, node))?;
145            if let Some(name) = name {
146                dropped.push(Dropped {
147                    name,
148                    construct: seed.construct.as_str(),
149                    spec_reference: seed.spec_reference,
150                    detail: seed.detail,
151                });
152            }
153        }
154        Ok(dropped)
155    }
156
157    /// The types that a declaration outside the type graph names: a
158    /// constant, an operation's result, a parameter, an attribute, an
159    /// annotation member or a topic.
160    fn types_named_outside_the_type_graph(&self) -> BTreeSet<NodeId> {
161        let mut named: BTreeSet<NodeId> = BTreeSet::new();
162        let mut note = |reference: &zerodds_ast_core::Ref<crate::TypeNode>| {
163            if let Some(id) = reference.resolved() {
164                if let Ok(node) = self.types.get(id) {
165                    named.insert(node.node_id);
166                }
167            }
168        };
169        for (_, constant) in self.constants.iter() {
170            note(&constant.type_);
171        }
172        for (_, operation) in self.operations.iter() {
173            if let Some(result) = &operation.return_type {
174                note(result);
175            }
176        }
177        for (_, parameter) in self.params.iter() {
178            note(&parameter.type_);
179        }
180        for (_, attribute) in self.attributes.iter() {
181            note(&attribute.type_);
182        }
183        for (_, member) in self.annotation_members.iter() {
184            note(&member.type_);
185        }
186        for (_, topic) in self.topics.iter() {
187            note(&topic.type_);
188        }
189        named
190    }
191}