Skip to main content

zerodds_lang_cst/
common.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! What the four trees share. Spec §1.
4//!
5//! Only what is genuinely common: an identifier, a path, a doc comment,
6//! a literal. Everything else differs between the languages and lives in
7//! their own modules — a shared `Type` would be a shared target model
8//! under another name.
9
10use alloc::string::{String, ToString};
11use alloc::vec::Vec;
12
13/// Which language a tree belongs to.
14///
15/// Carried so a coverage table can be indexed by it and a diagnostic can
16/// name it; the trees themselves never branch on it.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub enum Language {
19    /// Rust.
20    Rust,
21    /// C++.
22    Cpp,
23    /// C.
24    C,
25    /// Java.
26    Java,
27    /// C#.
28    CSharp,
29    /// Python.
30    Python,
31    /// TypeScript.
32    TypeScript,
33    /// Go.
34    Go,
35    /// OCaml.
36    OCaml,
37    /// Ada.
38    Ada,
39    /// D.
40    D,
41    /// Elixir.
42    Elixir,
43    /// Julia.
44    Julia,
45    /// Lua.
46    Lua,
47    /// Nim.
48    Nim,
49    /// Swift.
50    Swift,
51    /// Zig.
52    Zig,
53    /// Node (plain JavaScript).
54    Node,
55    /// Ada 83 (ANSI/MIL-STD-1815A), a second column for a language
56    /// already on the list — it answers enough clauses differently to
57    /// need its own materialisation.
58    Ada83,
59    /// F#.
60    FSharp,
61    /// Kotlin.
62    Kotlin,
63}
64
65impl Language {
66    /// Every language these trees cover.
67    ///
68    /// The languages whose backends are **complete**, so a coverage
69    /// table over this array is total by construction.
70    ///
71    /// A variant exists in `Language` before it is in here: the enum is
72    /// what a module registers itself under while it is being built, and
73    /// this array is what says it is done. A language added to the enum
74    /// and not to this list appears in no table and claims no coverage —
75    /// which is the honest state for one whose catalogue is still empty.
76    pub const ALL: [Self; 21] = [
77        Self::Rust,
78        Self::Cpp,
79        Self::C,
80        Self::Java,
81        Self::CSharp,
82        Self::Python,
83        Self::TypeScript,
84        Self::Go,
85        Self::OCaml,
86        Self::Ada,
87        Self::D,
88        Self::Elixir,
89        Self::Julia,
90        Self::Lua,
91        Self::Nim,
92        Self::Swift,
93        Self::Zig,
94        Self::Node,
95        Self::Ada83,
96        Self::FSharp,
97        Self::Kotlin,
98    ];
99
100    /// The name a diagnostic or a table column uses.
101    #[must_use]
102    pub const fn as_str(self) -> &'static str {
103        match self {
104            Self::Rust => "rust",
105            Self::Cpp => "c++",
106            Self::C => "c",
107            Self::Java => "java",
108            Self::CSharp => "c#",
109            Self::Python => "python",
110            Self::TypeScript => "typescript",
111            Self::Go => "go",
112            Self::OCaml => "ocaml",
113            Self::Ada => "ada",
114            Self::D => "d",
115            Self::Elixir => "elixir",
116            Self::Julia => "julia",
117            Self::Lua => "lua",
118            Self::Nim => "nim",
119            Self::Swift => "swift",
120            Self::Zig => "zig",
121            Self::Node => "node",
122            Self::Ada83 => "ada83",
123            Self::FSharp => "f#",
124            Self::Kotlin => "kotlin",
125        }
126    }
127
128    /// The file extension generated output takes.
129    #[must_use]
130    pub const fn extension(self) -> &'static str {
131        match self {
132            Self::Rust => "rs",
133            Self::Cpp => "hpp",
134            Self::C => "h",
135            Self::Java => "java",
136            Self::CSharp => "cs",
137            Self::Python => "py",
138            Self::TypeScript => "ts",
139            Self::Go => "go",
140            Self::OCaml => "ml",
141            Self::Ada => "ads",
142            Self::D => "d",
143            Self::Elixir => "ex",
144            Self::Julia => "jl",
145            Self::Lua => "lua",
146            Self::Nim => "nim",
147            Self::Swift => "swift",
148            Self::Zig => "zig",
149            Self::Node => "js",
150            Self::Ada83 => "ads",
151            Self::FSharp => "fs",
152            Self::Kotlin => "kt",
153        }
154    }
155}
156
157/// An identifier in a target language.
158///
159/// Holds the text as it will be written. Escaping a name that collides
160/// with a target keyword is a rule's job, not the identifier's: a type
161/// that silently renamed what it was given would hide the collision that
162/// made it necessary.
163#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
164pub struct Ident(String);
165
166impl Ident {
167    /// An identifier spelled exactly `text`.
168    #[must_use]
169    pub fn new(text: &str) -> Self {
170        Self(text.to_string())
171    }
172
173    /// How it is spelled.
174    #[must_use]
175    pub fn as_str(&self) -> &str {
176        &self.0
177    }
178
179    /// Whether it is empty, which no rule should produce.
180    #[must_use]
181    pub fn is_empty(&self) -> bool {
182        self.0.is_empty()
183    }
184}
185
186/// A qualified name, outermost segment first.
187///
188/// One type for `std::vector`, `java.util.List` and `::Robot::Pose`: the
189/// separator is the renderer's business, the segments are the tree's.
190#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
191pub struct Path {
192    segments: Vec<Ident>,
193    rooted: bool,
194}
195
196impl Path {
197    /// A path over `segments`, relative to whatever scope it appears in.
198    #[must_use]
199    pub fn new(segments: &[&str]) -> Self {
200        Self {
201            segments: segments.iter().map(|s| Ident::new(s)).collect(),
202            rooted: false,
203        }
204    }
205
206    /// A path from the root: `::A::B` in C++, `crate::a::B` in Rust.
207    #[must_use]
208    pub fn rooted(segments: &[&str]) -> Self {
209        Self {
210            segments: segments.iter().map(|s| Ident::new(s)).collect(),
211            rooted: true,
212        }
213    }
214
215    /// A path of one segment.
216    #[must_use]
217    pub fn single(name: &str) -> Self {
218        Self::new(&[name])
219    }
220
221    /// Its segments, outermost first.
222    #[must_use]
223    pub fn segments(&self) -> &[Ident] {
224        &self.segments
225    }
226
227    /// Whether it starts at the root.
228    #[must_use]
229    pub const fn is_rooted(&self) -> bool {
230        self.rooted
231    }
232
233    /// The last segment, which is the name itself.
234    #[must_use]
235    pub fn last(&self) -> Option<&Ident> {
236        self.segments.last()
237    }
238
239    /// The same path with one more segment on the end.
240    #[must_use]
241    pub fn join(&self, name: &str) -> Self {
242        let mut out = self.clone();
243        out.segments.push(Ident::new(name));
244        out
245    }
246}
247
248/// A documentation comment, one line per entry.
249///
250/// Lines rather than one string: the renderer prefixes each with the
251/// marker its language uses, and a string with embedded newlines would
252/// make that the renderer's parsing problem.
253#[derive(Debug, Clone, Default, PartialEq, Eq)]
254pub struct Doc {
255    lines: Vec<String>,
256}
257
258impl Doc {
259    /// An empty documentation comment, which renders as nothing.
260    #[must_use]
261    pub fn none() -> Self {
262        Self::default()
263    }
264
265    /// A documentation comment of one line.
266    #[must_use]
267    pub fn line(text: &str) -> Self {
268        Self {
269            lines: alloc::vec![text.to_string()],
270        }
271    }
272
273    /// A documentation comment over several lines.
274    #[must_use]
275    pub fn lines(lines: &[&str]) -> Self {
276        Self {
277            lines: lines.iter().map(|line| (*line).to_string()).collect(),
278        }
279    }
280
281    /// Add a line.
282    pub fn push(&mut self, text: &str) {
283        self.lines.push(text.to_string());
284    }
285
286    /// Its lines.
287    #[must_use]
288    pub fn as_lines(&self) -> &[String] {
289        &self.lines
290    }
291
292    /// Whether there is nothing to render.
293    #[must_use]
294    pub fn is_empty(&self) -> bool {
295        self.lines.is_empty()
296    }
297}
298
299/// `@verbatim` text inside a declaration's body.
300///
301/// OMG IDL 4.2 §8.3.5.1's `BEGIN_DECLARATION` and `END_DECLARATION`
302/// placements, for a declaration whose body is a list of fields or
303/// properties and has no member variant to hold a raw line. Copied
304/// unchanged, one output line per text line, at the body's indentation.
305#[derive(Debug, Clone, Default, PartialEq, Eq)]
306pub struct BodyText {
307    /// Text written first inside the body.
308    pub begin: Vec<String>,
309    /// Text written last inside the body.
310    pub end: Vec<String>,
311}
312
313impl BodyText {
314    /// No text, which is the common case.
315    #[must_use]
316    pub fn none() -> Self {
317        Self::default()
318    }
319}
320
321/// A literal value, in the form it will be written.
322///
323/// The written form rather than a number: `0x1F` and `31` are the same
324/// value and different source, and a generator reproducing a constant
325/// has to write the one that was there.
326#[derive(Debug, Clone, PartialEq, Eq)]
327pub enum Literal {
328    /// An integer, as written.
329    Int(String),
330    /// A floating-point number, as written.
331    Float(String),
332    /// A boolean.
333    Bool(bool),
334    /// A character.
335    Char(char),
336    /// A string, unescaped — the renderer escapes for its language.
337    Str(String),
338    /// A byte string, for the languages that have one.
339    Bytes(Vec<u8>),
340    /// The language's null or none.
341    Null,
342}
343
344impl Literal {
345    /// An integer literal from a value, in decimal.
346    #[must_use]
347    pub fn int(value: i128) -> Self {
348        Self::Int(alloc::format!("{value}"))
349    }
350
351    /// A string literal.
352    #[must_use]
353    pub fn string(text: &str) -> Self {
354        Self::Str(text.to_string())
355    }
356}
357
358/// How visible a declaration is.
359///
360/// The three states the four languages between them need. A language
361/// that has no word for one renders it as nothing.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
363pub enum Visibility {
364    /// Visible outside its unit: `pub`, `public`.
365    Public,
366    /// Visible within its unit: `pub(crate)`, package-private, `protected`.
367    Internal,
368    /// Not visible outside its declaration: `private`, no modifier in Rust.
369    #[default]
370    Private,
371}
372
373impl Visibility {
374    /// Every visibility the trees distinguish.
375    pub const ALL: [Self; 3] = [Self::Public, Self::Internal, Self::Private];
376}
377
378#[cfg(test)]
379mod tests {
380    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
381    use super::*;
382
383    #[test]
384    fn a_path_keeps_its_segments_in_order_and_knows_whether_it_is_rooted() {
385        let path = Path::rooted(&["Robot", "Pose"]);
386        assert!(path.is_rooted());
387        assert_eq!(path.segments().len(), 2);
388        assert_eq!(path.last().map(Ident::as_str), Some("Pose"));
389
390        let joined = path.join("Inner");
391        assert_eq!(joined.segments().len(), 3);
392        assert!(joined.is_rooted(), "joining lost the root");
393    }
394
395    #[test]
396    fn an_identifier_is_written_exactly_as_it_was_given() {
397        // Escaping a keyword collision is a rule's decision; a type that
398        // renamed silently would hide what made it necessary.
399        for name in ["Pose", "type", "class", "_reserved"] {
400            assert_eq!(Ident::new(name).as_str(), name);
401        }
402    }
403
404    #[test]
405    fn every_language_has_its_own_name_and_extension() {
406        let mut seen = alloc::vec::Vec::new();
407        for language in Language::ALL {
408            assert!(!seen.contains(&language.as_str()));
409            seen.push(language.as_str());
410            assert!(!language.extension().is_empty());
411        }
412        assert_eq!(seen.len(), Language::ALL.len());
413    }
414
415    #[test]
416    fn a_doc_comment_keeps_its_lines_apart() {
417        let doc = Doc::lines(&["first", "second"]);
418        assert_eq!(doc.as_lines().len(), 2);
419        assert!(!doc.is_empty());
420        assert!(Doc::none().is_empty());
421    }
422}