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(¶meter.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}