zerodds_wire_probe/target_keywords.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The identifiers a target language will not let a generator paste
4//! through, and what each generator renames them to.
5//!
6//! IDL and its target languages do not share a reserved word list. CORBA
7//! 3.3 part 1 §7.2.3 gives IDL twenty-odd keywords and says nothing
8//! about `new`, `static` or `class`; all three are legal member names in
9//! an IDL file that every front end on the test host accepts. The
10//! generated header is another matter — `long new;` is not a C++
11//! declaration — so each vendor's IDL compiler renames the identifier on
12//! its way into the target language, and the fill and dump emitters here
13//! have to spell it the way the vendor spelled it or the probe does not
14//! compile.
15//!
16//! There is no single rule to apply. The prefix, and the list of names
17//! it applies to, is the *generator's*, so each of the three below is a
18//! measurement of one generator's output over
19//! `tests/interop-corpus/identifier-target-keywords/type.idl` and not a
20//! rule derived from a specification:
21//!
22//! | function | generator | measured output |
23//! |---|---|---|
24//! | [`cxx`] | omniidl 4.3.3, `tao_idl` 2.5.24 | `_cxx_new`, `_cxx_delete`, `_cxx_operator`, `_cxx_static`, `enum _cxx_class { _cxx_template, _cxx_virtual, _cxx_friend }` |
25//! | [`c`] | `rtiddsgen -language C`, Connext 7.7 | `DDS_Long _c_static;`, beside `DDS_Long new;` and `DDS_Double delete;` |
26//! | [`java`] | JacORB 3.9 | module `package` becomes the Java package `_package`, `enum class` the class `_class` |
27//!
28//! All three were taken 2026-08-11 on the Linux test host, from each
29//! generator's own output for that corpus case.
30//!
31//! ## The whole class, and which cell of it is still open
32//!
33//! Three functions here, seven emitter-and-vendor pairs, and the
34//! question is the same for each: does the vendor's generator rename,
35//! and does the emitter that writes against its header do the same?
36//! Measured 2026-08-11 and 2026-08-12 on the Linux test host over the
37//! three `identifier-target-keyword-*` cases.
38//!
39//! | emitter | vendors | the generator renames | the emitter renames | how the position is reached |
40//! |---|---|---|---|---|
41//! | `fill.rs`, `dump.rs` | Cyclone | no | no | `wireprobe-batch` wraps the case in `module zd_cN`, so the C name is `zd_c1_static` and no position is reached |
42//! | `fill.rs`, `dump.rs` | RTI | `_c_` | yes | the type name and the enumerator, both measured |
43//! | `fill_cpp.rs`, `dump_cpp.rs` | Fast DDS | no | no | reached, and `fastddsgen`'s own C++ is `const zd_c5::static& data);`, which no compiler takes — recorded as its refusal |
44//! | `corba_cpp.rs` | omniORB, TAO | `_cxx_` | yes | the type name, `_cxx_static` |
45//! | `corba_java.rs` | JacORB | `_` | yes | the type name, `_static` |
46//! | `fill_ros.rs`, `dump_ros.rs`, `ros_idl.rs` | ros-fastrtps, ros-cyclone | — | nothing to apply | rosidl's own convention gives `zdros__msg__Static`, which is not a keyword in any of its target languages |
47//!
48//! Six of the seven agreed with their generator. The one that did not
49//! was the C pair, and it did not agree with *itself*: `fill.rs` put
50//! every identifier through [`c`] and `dump.rs` pasted the IDL
51//! spelling, so for RTI the two halves of one translation unit named
52//! one enumerator two ways. Both halves do it now.
53//!
54//! The open cell is the bitmask flag. `rtiddsgen` refuses every bitmask
55//! in the corpus at the front end and Cyclone's flag is inside the
56//! batch's module, so no C column can be asked; Fast DDS refuses the
57//! case; the ORBs have no bitmask. It is recorded as unmeasured in
58//! `tests/interop-corpus/identifier-target-keyword-bitmask-flag`.
59//!
60//! ## Why the three lists are not one list
61//!
62//! The measurement is the reason. `new` and `delete` are C++ keywords
63//! and not C ones, and `rtiddsgen -language C` leaves both verbatim
64//! while renaming `static`, which is a keyword in both — so a C emitter
65//! that shared the C++ list would rename two members the vendor did not
66//! and stop agreeing with the vendor's own header. `template`,
67//! `virtual` and `friend` run the other way: renamed by both C++ front
68//! ends, left alone by JacORB, because Java has none of the three.
69//!
70//! ## Two generators that rename nothing
71//!
72//! Measured the same day and worth writing down, because "no escaping"
73//! is a finding and not an omission: Cyclone's `idlc` emits
74//! `int32_t new;` and `int32_t static;` verbatim, and `fastddsgen`
75//! escapes nothing either. Neither has a function here, and the
76//! emitters that serve them — `fill.rs`'s [`Vendor::Cyclone`] arm and
77//! `fill_cpp.rs` — paste the IDL identifier through unchanged.
78//!
79//! [`Vendor::Cyclone`]: crate::fill::Vendor::Cyclone
80//!
81//! ## Where these belong and where they do not
82//!
83//! An identifier position in generated target-language source, and
84//! nowhere else. The IDL the probe hands the vendor's own front end
85//! carries the identifier the corpus wrote — that file is the input the
86//! renaming is a *consequence* of — and so do the JSON keys the dump
87//! emitters print, the paths in a [`ProbeError`] and every string
88//! literal. Renaming a JSON key would make the ZeroDDS column disagree
89//! with every vendor column over a member name that is not on the wire
90//! at all.
91//!
92//! [`ProbeError`]: crate::encode::ProbeError
93
94use std::borrow::Cow;
95
96/// The C++ spelling of an IDL identifier, as omniORB and TAO generate
97/// it.
98///
99/// A C++ keyword comes back with `_cxx_` in front of it and everything
100/// else comes back untouched. Measured 2026-08-11 from omniidl's and
101/// `tao_idl`'s output for
102/// `tests/interop-corpus/identifier-target-keywords`: the members `new`,
103/// `delete`, `operator` and `static` are generated as `_cxx_new`,
104/// `_cxx_delete`, `_cxx_operator` and `_cxx_static`, the enumeration
105/// `class` as `enum _cxx_class` with the enumerators `_cxx_template`,
106/// `_cxx_virtual` and `_cxx_friend` — and `package`, `type`, `impl`,
107/// `lambda`, `def` and `async`, which are keywords in some other
108/// language and none in C++, appear verbatim in the same header. Both
109/// front ends produce the same spelling, which is what lets one emitter
110/// serve both.
111///
112/// The list is C++23's, plus C's, because a C keyword that C++ dropped
113/// would still be one the C++ front end has to rename and no corpus case
114/// has yet asked which. The two overlap almost entirely; `restrict` and
115/// the `_Atomic` family are the difference.
116#[must_use]
117pub fn cxx(name: &str) -> Cow<'_, str> {
118 if is_listed(CXX_KEYWORDS, name) || is_listed(C_KEYWORDS, name) {
119 Cow::Owned(format!("_cxx_{name}"))
120 } else {
121 Cow::Borrowed(name)
122 }
123}
124
125/// The C spelling of an IDL identifier, as `rtiddsgen` generates it.
126///
127/// A C keyword comes back with `_c_` in front of it. Measured
128/// 2026-08-11 from `rtiddsgen -language C` (Connext 7.7) over
129/// `tests/interop-corpus/identifier-target-keywords`: the generated
130/// structure carries `DDS_Long _c_static;` beside `DDS_Long new;` and
131/// `DDS_Double delete;`, which is the whole of why this is a separate
132/// function from [`cxx`] — `new` and `delete` are C++ keywords, C has
133/// neither, and RTI's C generator renames by the C list alone.
134///
135/// Two more positions were measured the same day, each from its own
136/// corpus case, because a member is not the only place an identifier
137/// reaches the generated source:
138///
139/// | position | case | what rtiddsgen generated |
140/// |---|---|---|
141/// | the type name | `identifier-target-keyword-typename` | `_c_static`, and every symbol derived from it — the compiler named the spelling itself, *did you mean `_c_staticPlugin_get_serialized_sample_max_size`?* |
142/// | an enumerator | `identifier-target-keyword-enumerator` | `_c_volatile`; the probe's write side already spelled it that way and compiled |
143///
144/// The third, a bitmask flag, has no measurement: `rtiddsgen` refuses
145/// every bitmask in the corpus at the front end, so
146/// `identifier-target-keyword-bitmask-flag` records the position as
147/// unmeasured rather than as agreed.
148///
149/// The list is applied to the **flattened** name and not to each
150/// segment of it. `::package::static` is `package_static`, which C does
151/// not reserve; only a declaration at the outermost scope keeps a
152/// spelling a compiler reads as a keyword.
153///
154/// The list is C23's. `bool`, `true`, `false` and `static_assert` became
155/// keywords in that revision rather than macros, and a generator built
156/// against a newer front end would rename them; no corpus case has one,
157/// so the wider list is the safe direction and the narrower one would be
158/// the guess.
159#[must_use]
160pub fn c(name: &str) -> Cow<'_, str> {
161 if is_listed(C_KEYWORDS, name) {
162 Cow::Owned(format!("_c_{name}"))
163 } else {
164 Cow::Borrowed(name)
165 }
166}
167
168/// The Java spelling of an IDL identifier, as JacORB generates it.
169///
170/// A Java keyword comes back with a single `_` in front of it. Measured
171/// 2026-08-11 from JacORB 3.9's output for
172/// `tests/interop-corpus/identifier-target-keywords`: the IDL module
173/// `package` becomes the Java package `_package` and the enumeration
174/// `class` the class `_class`, while the enumerators `template`,
175/// `virtual` and `friend` keep their spelling, because Java has no such
176/// keywords. One underscore, and one is the whole prefix — the same
177/// output carries `_package` and not `_java_package`, which is why this
178/// is a third function and not [`c`] with another string in front.
179///
180/// The list is Java SE's fifty reserved words plus the three literals
181/// `true`, `false` and `null`, which §3.9 of the Java Language
182/// Specification keeps out of the keyword list but which are just as
183/// unusable as an identifier. The contextual words — `var`, `record`,
184/// `sealed`, `yield` — are legal identifiers and are deliberately absent.
185#[must_use]
186pub fn java(name: &str) -> Cow<'_, str> {
187 if is_listed(JAVA_KEYWORDS, name) {
188 Cow::Owned(format!("_{name}"))
189 } else {
190 Cow::Borrowed(name)
191 }
192}
193
194/// The Rust spelling of an IDL identifier, as `hddsgen` generates it.
195///
196/// A Rust keyword comes back as a raw identifier. Measured 2026-08-16
197/// from `hddsgen gen rust` over `tests/interop-corpus/recursive-mutual`,
198/// whose IDL declares `sequence<A> as`: the generated field is
199/// `pub r#as: Vec<A>`. An emitter that wrote the bare name produced
200/// `expected identifier, found keyword` and no octets — which reads on
201/// the matrix as HDDS refusing a case it in fact generated.
202///
203/// The list is Rust 2021's, strict and reserved together: a reserved
204/// word is not usable as an identifier either, and `r#` is correct for
205/// both. `self`, `super`, `crate` and `Self` are the exception — they
206/// cannot be raw identifiers at all — and are left alone, because a
207/// field so named is a construct hddsgen would have to rename itself
208/// and no corpus case has asked which way.
209#[must_use]
210pub fn rust(name: &str) -> Cow<'_, str> {
211 const NOT_RAW_ABLE: &[&str] = &["Self", "crate", "self", "super"];
212 if is_listed(RUST_KEYWORDS, name) && !is_listed(NOT_RAW_ABLE, name) {
213 Cow::Owned(format!("r#{name}"))
214 } else {
215 Cow::Borrowed(name)
216 }
217}
218
219/// Whether `name` is in a sorted keyword list.
220///
221/// A binary search, which is why each list below carries a test that it
222/// is sorted: an unsorted list would not fail loudly, it would miss one
223/// keyword and emit source the vendor's compiler refuses, which reads on
224/// the matrix as a vendor difference.
225fn is_listed(list: &[&str], name: &str) -> bool {
226 list.binary_search(&name).is_ok()
227}
228
229/// The keywords of C23, ISO/IEC 9899:2024 §6.4.1, sorted.
230///
231/// The `_`-prefixed spellings sort first, which is byte order and not an
232/// accident of typing.
233const C_KEYWORDS: &[&str] = &[
234 "_Alignas",
235 "_Alignof",
236 "_Atomic",
237 "_BitInt",
238 "_Bool",
239 "_Complex",
240 "_Decimal128",
241 "_Decimal32",
242 "_Decimal64",
243 "_Generic",
244 "_Imaginary",
245 "_Noreturn",
246 "_Static_assert",
247 "_Thread_local",
248 "alignas",
249 "alignof",
250 "auto",
251 "bool",
252 "break",
253 "case",
254 "char",
255 "const",
256 "constexpr",
257 "continue",
258 "default",
259 "do",
260 "double",
261 "else",
262 "enum",
263 "extern",
264 "false",
265 "float",
266 "for",
267 "goto",
268 "if",
269 "inline",
270 "int",
271 "long",
272 "nullptr",
273 "register",
274 "restrict",
275 "return",
276 "short",
277 "signed",
278 "sizeof",
279 "static",
280 "static_assert",
281 "struct",
282 "switch",
283 "thread_local",
284 "true",
285 "typedef",
286 "typeof",
287 "typeof_unqual",
288 "union",
289 "unsigned",
290 "void",
291 "volatile",
292 "while",
293];
294
295/// The keywords of C++23, ISO/IEC 14882:2024 Table 5, sorted.
296///
297/// The alternative tokens of §5.5 — `and`, `bitand`, `not_eq` and the
298/// rest — are in the table and therefore in the list. They are spelled
299/// like ordinary identifiers, which is exactly why an IDL member called
300/// `xor` would slip past a list that left them out.
301///
302/// `export`, `register` and the other words the revisions have retired
303/// stay in: a front end built against an older standard renames them,
304/// and a name this list left out is a probe that does not compile
305/// against that front end.
306const CXX_KEYWORDS: &[&str] = &[
307 "alignas",
308 "alignof",
309 "and",
310 "and_eq",
311 "asm",
312 "auto",
313 "bitand",
314 "bitor",
315 "bool",
316 "break",
317 "case",
318 "catch",
319 "char",
320 "char16_t",
321 "char32_t",
322 "char8_t",
323 "class",
324 "co_await",
325 "co_return",
326 "co_yield",
327 "compl",
328 "concept",
329 "const",
330 "const_cast",
331 "consteval",
332 "constexpr",
333 "constinit",
334 "continue",
335 "decltype",
336 "default",
337 "delete",
338 "do",
339 "double",
340 "dynamic_cast",
341 "else",
342 "enum",
343 "explicit",
344 "export",
345 "extern",
346 "false",
347 "float",
348 "for",
349 "friend",
350 "goto",
351 "if",
352 "inline",
353 "int",
354 "long",
355 "mutable",
356 "namespace",
357 "new",
358 "noexcept",
359 "not",
360 "not_eq",
361 "nullptr",
362 "operator",
363 "or",
364 "or_eq",
365 "private",
366 "protected",
367 "public",
368 "register",
369 "reinterpret_cast",
370 "requires",
371 "return",
372 "short",
373 "signed",
374 "sizeof",
375 "static",
376 "static_assert",
377 "static_cast",
378 "struct",
379 "switch",
380 "template",
381 "this",
382 "thread_local",
383 "throw",
384 "true",
385 "try",
386 "typedef",
387 "typeid",
388 "typename",
389 "union",
390 "unsigned",
391 "using",
392 "virtual",
393 "void",
394 "volatile",
395 "wchar_t",
396 "while",
397 "xor",
398 "xor_eq",
399];
400
401/// The reserved words of Java SE, JLS §3.9, plus the three literals of
402/// §3.10.3 and §3.10.8, sorted.
403///
404/// `const` and `goto` are reserved and unused, and are in the list for
405/// the reason the retired C++ words are: `javac` refuses them as
406/// identifiers whatever the compiler does with them afterwards.
407const JAVA_KEYWORDS: &[&str] = &[
408 "abstract",
409 "assert",
410 "boolean",
411 "break",
412 "byte",
413 "case",
414 "catch",
415 "char",
416 "class",
417 "const",
418 "continue",
419 "default",
420 "do",
421 "double",
422 "else",
423 "enum",
424 "extends",
425 "false",
426 "final",
427 "finally",
428 "float",
429 "for",
430 "goto",
431 "if",
432 "implements",
433 "import",
434 "instanceof",
435 "int",
436 "interface",
437 "long",
438 "native",
439 "new",
440 "null",
441 "package",
442 "private",
443 "protected",
444 "public",
445 "return",
446 "short",
447 "static",
448 "strictfp",
449 "super",
450 "switch",
451 "synchronized",
452 "this",
453 "throw",
454 "throws",
455 "transient",
456 "true",
457 "try",
458 "void",
459 "volatile",
460 "while",
461];
462
463/// The keywords of Rust 2021, strict and reserved together, sorted.
464///
465/// Reserved words are in because they are equally unusable as a bare
466/// identifier, and `r#` is the right answer for both.
467const RUST_KEYWORDS: &[&str] = &[
468 "Self", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue",
469 "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl",
470 "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
471 "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof",
472 "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
473];
474
475#[cfg(test)]
476mod tests {
477 use super::{C_KEYWORDS, CXX_KEYWORDS, JAVA_KEYWORDS, c, cxx, java};
478
479 /// `is_listed` is a binary search, so an unsorted list would answer
480 /// "not a keyword" for a keyword and the emitter would paste it
481 /// through.
482 #[test]
483 fn every_list_is_sorted() {
484 for list in [C_KEYWORDS, CXX_KEYWORDS, JAVA_KEYWORDS] {
485 let mut sorted = list.to_vec();
486 sorted.sort_unstable();
487 assert_eq!(list, sorted.as_slice());
488 sorted.dedup();
489 assert_eq!(list.len(), sorted.len(), "a keyword is listed twice");
490 }
491 }
492
493 /// The three prefixes are the generators', and each is a different
494 /// generator's.
495 #[test]
496 fn each_target_keeps_its_own_prefix() {
497 assert_eq!(cxx("class"), "_cxx_class");
498 assert_eq!(c("static"), "_c_static");
499 assert_eq!(java("package"), "_package");
500 }
501
502 /// A name no target reserves is borrowed rather than rebuilt, which
503 /// is the case every corpus member but a handful takes.
504 #[test]
505 fn an_ordinary_name_is_passed_through_unallocated() {
506 assert!(matches!(cxx("impl"), std::borrow::Cow::Borrowed("impl")));
507 assert!(matches!(c("impl"), std::borrow::Cow::Borrowed("impl")));
508 assert!(matches!(java("impl"), std::borrow::Cow::Borrowed("impl")));
509 }
510}