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}