zerodds_lang_rules/adapter/csharp.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The C# CST as the other end of a rule. Spec §3.2.
4//!
5//! The counterpart of [`crate::adapter::java`], which does the same for
6//! Java, of [`crate::adapter::c`] for C and of [`crate::adapter::cst`]
7//! for Rust. Reading two of them side by side is the point: where they
8//! differ, the difference is the language's, and it is written down here
9//! rather than spread through a renderer.
10//!
11//! Values are resolved from the model, never from the overlay — the same
12//! rule the other materialisers follow and for the same reason
13//! (spec §2.3).
14//!
15//! ## The four decisions this column makes
16//!
17//! ### 1. An IDL `struct` becomes a C# `class`, not a C# `struct`
18//!
19//! Three reasons, any one of which is enough:
20//!
21//! - **A `struct` is copied.** C# assigns and passes a `struct` by value.
22//! A codec handed a sample to fill writes into the copy, the caller
23//! keeps the empty original, and nothing reports anything. That is the
24//! silent-drop class this repository is arranged against, and it is a
25//! language property rather than a coding mistake anyone could avoid.
26//! - **A `struct` cannot be `null`.** `@optional` is absence, and
27//! `null` is what absence is here (see below). A holder that has no
28//! `null` needs a second field to say "not present", and two fields
29//! that can disagree is exactly what the Java column refused for the
30//! same construct.
31//! - **A `struct` may not contain itself.** CS0523 forbids an instance
32//! field whose type is the enclosing struct, directly or through
33//! another struct, and IDL reaches that shape through `@external` and
34//! through a boxed value.
35//!
36//! The cost is stated rather than hidden: a small aggregate is heap
37//! allocated where a `struct` would not be. That is a throughput
38//! question with a measurable answer; a value silently written into a
39//! copy is not.
40//!
41//! `Float128` below is the one generated `struct`, because it is a
42//! primitive: sixteen octets, never null, never recursive.
43//!
44//! ### 2. An IDL module becomes a `namespace`
45//!
46//! Not a nested class, which is what the Java column had to do. Two
47//! Java rules forced that one: a file's public top-level type must be
48//! named after the file, and a file may hold only one. C# has neither —
49//! a compilation unit declares as many top-level types as it likes under
50//! any file name — so the scope IDL declares is a scope here too, and
51//! `module a { struct P; }` and `module b { struct P; }` are two types
52//! with no outer class between them.
53//!
54//! Every reference to a generated type is written **rooted**:
55//! `global::a.b.Point`. C# separates type lookup from expression lookup,
56//! so a field named `a` cannot capture the type `a.b.Point` the way JLS
57//! §6.5.2 lets it in Java — but a *nested* namespace can: inside
58//! `namespace common.a`, the name `a` binds to `common.a` before the
59//! global `a`. `global::` resolves at the global namespace and nothing
60//! in any scope can capture it, which is why every path this
61//! materialiser writes carries it.
62//!
63//! ### 3. Members are `PascalCase` fields carrying their IDL name
64//!
65//! **Fields, not properties.** A property cannot be passed as `ref` or
66//! `out` (CS0206), and a decoder that fills a member in place needs
67//! exactly that. `init`-only properties on a `record` cannot be filled
68//! incrementally at all.
69//!
70//! **`PascalCase`, because .NET names public members that way** and
71//! every C# consumer already in this repository —
72//! `zerodds-examples/csharp-typed`, `endpoints/csharp` — writes them so.
73//!
74//! **Only the first letter moves.** `venue` becomes `Venue` and
75//! `my_field` becomes `My_field`. A full `PascalCase` fold —
76//! `my_field` to `MyField` — is not injective, and IDL 4.2 §7.2.3 forbids
77//! two identifiers in one scope that differ only in case but permits
78//! `my_field` beside `myField`; both would fold to one C# name and one of
79//! the two members would disappear. Raising the first letter alone is
80//! injective over any scope IDL allows.
81//!
82//! **The IDL spelling is carried, on every field, in
83//! `[global::ZeroDds.Idl.IdlName("…")]`.** This is not decoration and
84//! not a comment. XTypes 1.3 §7.3.1.2.1.1 computes a member's ID under
85//! `@autoid(HASH)` and under `@hashid` from the **IDL member name** — the
86//! first four octets of its MD5 — so a type that only knows `Venue`
87//! cannot compute the ID for `venue`, and the member it writes lands
88//! under a different ID than the peer's. The precedent is
89//! `crates/dlrl-codegen/src/csharp.rs`, which carries `[DlrlKey("venue")]`
90//! for the same reason one layer up: the DCPS topic names the key
91//! `venue`.
92//!
93//! Type, module and enumerator names are **not** folded. They are
94//! verbatim but for keyword escaping, because a type's name reaches the
95//! wire — `TypeObject`'s `type_name`, and the topic type name a peer
96//! matches on — and a renamed type is a type nothing matches.
97//!
98//! ### 4. `@optional` is `null`, and a value type takes `T?`
99//!
100//! One member, not a member beside a flag: a `bool _has_m` and an `m`
101//! are two things that can disagree and nothing in the type system stops
102//! them. A reference type is nullable already; a value type — every
103//! numeric, `char`, `bool`, a generated `enum`, `Float128` — takes
104//! `Nullable<T>`, written `T?`. No `#nullable enable` is emitted, so `?`
105//! appears only where it is a distinct type and never as an annotation
106//! the compiler would warn about outside an annotation context (CS8632).
107//!
108//! ## The integer mapping
109//!
110//! C# has unsigned integers, which is the largest single difference from
111//! the Java column: every IDL width maps to a C# keyword of the same
112//! width and the same signedness, nothing is held as a pattern, and
113//! nothing has to be widened to stay a number.
114//!
115//! | IDL | C# | width |
116//! |---|---|---|
117//! | `boolean` | `bool` | |
118//! | `char` | `char` | one UTF-16 code unit; the wire form is one octet |
119//! | `wchar` | `char` | one UTF-16 code unit |
120//! | `int8` | `sbyte` | 8, signed |
121//! | `octet`, `uint8` | `byte` | 8, unsigned |
122//! | `short` | `short` | 16, signed |
123//! | `unsigned short` | `ushort` | 16, unsigned |
124//! | `long` | **`int`** | 32, signed |
125//! | `unsigned long` | `uint` | 32, unsigned |
126//! | `long long` | **`long`** | 64, signed |
127//! | `unsigned long long` | `ulong` | 64, unsigned |
128//! | `float` | `float` | binary32 |
129//! | `double` | `double` | binary64 |
130//! | `long double` | `global::ZeroDds.Idl.Float128` | binary128 |
131//!
132//! The two rows in bold are the trap this mapping exists to avoid. IDL
133//! `long` is **32 bits** and C# `long` is **64**; IDL `long long` is 64
134//! and C# `int` is 32. A column that carried the spelling across would
135//! compile and write four octets where the peer reads eight.
136//!
137//! `long double` has no C# type at all. `decimal` is not it — `decimal`
138//! is a base-ten type with 28 to 29 significant digits and a scale, not
139//! IEEE 754 binary128 — so a sixteen-octet holder is generated instead
140//! and named for what it is.
141//!
142//! ## The shapes
143//!
144//! | IDL | C# |
145//! |---|---|
146//! | `string`, `wstring` | `string` |
147//! | `fixed<P,S>` | `string`, holding the decimal |
148//! | `sequence<T>`, `T[N]` | `T[]` |
149//! | `T[N][M]` | `T[][]` |
150//! | `map<K,V>` | `ZdEntry_k_v[]` — an array of a two-field class |
151//! | `union` | a class with `_d` and one field per case |
152//! | `enum` | a real `enum : int` |
153//! | `bitmask` | `[Flags] enum : <holder>` |
154//! | `bitset` | a class of one field per bitfield |
155//! | `typedef` | `using Name = T;` |
156//! | `native` | `using Name = T;`, from the facet or `object` |
157//! | `const` | a `const` field of a `ZdConstants` class |
158//! | `interface` | an `interface` of operation signatures |
159//!
160//! ### An enumeration is a real `enum`, which Java could not use
161//!
162//! The Java column maps an enumeration to `int` constants and gives two
163//! reasons. Neither holds in C#:
164//!
165//! - A Java `enum` constant's identity is its ordinal. A C# `enum`
166//! member's identity is the integer it is given, so `@value(-1)` and a
167//! `@bit_bound(32)` value are members like any other.
168//! - A value the reader's IDL does not list has no Java `enum` constant.
169//! In C# every value of the underlying type is a value of the enum
170//! type — `(Colour)99` is legal and holds — so an `@appendable` writer
171//! that added an enumerator sends something the field can carry.
172//!
173//! The underlying type is `int`, written out. XTypes 1.3 §7.4.3.5.3
174//! makes an enumerated literal 32 bits wide, and a value outside those
175//! 32 bits is refused by name rather than truncated.
176//!
177//! ### A bitmask is a `[Flags]` enum
178//!
179//! Typed rather than a bare integer, with the underlying type Table 33
180//! of XTypes 1.3 §7.4.1.1.3.2 gives for its bound: `byte`, `ushort`,
181//! `uint` or `ulong`. `[Flags]` is what makes `ToString` print the set
182//! rather than a number, and it changes nothing about the octets.
183//!
184//! ### A `map<K,V>` is not a `Dictionary<K,V>`
185//!
186//! The order of the entries is a policy axis — `MapEntryOrder` — and
187//! every `Dictionary` either hashes the order away or fixes one of its
188//! own. An array of a generated entry class keeps the sample's order
189//! where the sample put it, and the axis stays measurable. The entry
190//! class is named from the **IDL** kinds of its halves, not the C# ones,
191//! so that two maps whose C# spelling coincides and whose wire widths do
192//! not stay two classes.
193//!
194//! ### `fixed<P,S>` is a `string` and not a `decimal`
195//!
196//! `decimal` carries 28 to 29 significant digits; IDL 4.2 §7.4.1.4.4
197//! allows up to 31. Beyond that, the `FixedDigitCount::ValuePrecision`
198//! axis exists to measure which digit count a peer writes into the BCD
199//! run, and a holder that normalises has already decided the answer. The
200//! Java column reaches the same place from `BigDecimal`.
201//!
202//! ### A typedef is a `using` alias
203//!
204//! C# 12 aliases any type, arrays and tuples included, so `typedef
205//! sequence<long> Row` is `using Row = int[];` — a form Java has no
206//! counterpart for, where the Java column can only leave a comment.
207//!
208//! The alias is declared for the reader; **uses take the resolved
209//! type**. A `using` alias may not be built on another `using` alias in
210//! the same scope, so a chain `typedef long A; typedef A B;` could not be
211//! written as one, and resolving at each use keeps the two forms of
212//! typedef from behaving differently.
213//!
214//! ## What is refused
215//!
216//! `any`, `ValueBase`, an object reference and an annotation type have no
217//! C# data form here and are named as
218//! [`MaterialiseError::NoLanguageForm`] rather than left out of the
219//! class. A generator that leaves a member out emits a type missing a
220//! field and reports success.
221
222use crate::adapter::c::ordered_types;
223use crate::adapter::constant;
224use crate::adapter::cst::MaterialiseError;
225use crate::adapter::interface;
226use crate::apply::Applied;
227use crate::verbatim;
228use seesaw_tgg::ident::GhostId;
229use zerodds_ast::{Ast, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
230use zerodds_ast_core::{Id, NodeId};
231use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
232use zerodds_lang_cst::csharp;
233
234/// The namespace the generated support declarations sit in.
235///
236/// Emitted into the file rather than taken from a runtime assembly: a
237/// generated file is one `csc` argument, and a column whose output only
238/// compiles against a package that is not written yet would be a claim
239/// rather than a mapping.
240pub const SUPPORT_NAMESPACE: &[&str] = &["ZeroDds", "Idl"];
241
242/// The attribute that carries a member's IDL spelling.
243const IDL_NAME_ATTRIBUTE: &str = "IdlName";
244
245/// The generated holder for an IDL `long double`.
246const FLOAT128: &str = "Float128";
247
248/// The prefix a generated map-entry class carries.
249const ENTRY_PREFIX: &str = "ZdEntry_";
250
251/// The class a scope's IDL constants become members of.
252///
253/// C# has no namespace-level field, so a constant needs a type to live
254/// in. One static class per scope rather than one per constant: the
255/// names inside it are the IDL's own.
256const CONSTANTS_CLASS: &str = "ZdConstants";
257
258/// The names a generated declaration may not take at the global scope.
259///
260/// Every reference this materialiser writes is rooted at `global::`, and
261/// `global::ZeroDds` or `global::System` would then resolve to a
262/// generated namespace instead of the one meant. `global` itself is
263/// listed because a namespace of that name makes every rooted path in
264/// the file ambiguous to a reader even where the compiler copes.
265const RESERVED_TOP_LEVEL: &[&str] = &["ZeroDds", "System", "global"];
266
267/// How deep a module chain is followed before the model is taken to be
268/// malformed. IDL nests nowhere near this.
269const MAX_MODULE_DEPTH: usize = 64;
270
271/// How deep a type's C# spelling is followed.
272///
273/// A `sequence<sequence<…>>` nests once per level; a chain longer than
274/// this is a cycle the resolver did not catch, and recursing into it
275/// would end the process rather than report anything.
276const MAX_TYPE_DEPTH: usize = 64;
277
278/// How many times a colliding member name is suffixed before the model
279/// is taken to be malformed.
280///
281/// A member's C# name may have to move once — CS0542 forbids a member
282/// sharing its enclosing type's name — and a second time if the moved
283/// name has been taken too. A bound rather than a `loop`: an unbounded
284/// rename would hang on a model that somehow produced them all.
285const MAX_RENAME_ATTEMPTS: usize = 64;
286
287/// Build the C# file a rule application produced.
288///
289/// The whole file and not a list of declarations: `using` directives
290/// have to precede every declaration in their scope, so a signature that
291/// returned only declarations could not express a typedef at all.
292/// [`csharp::File::doc`] is left empty for the caller to fill.
293///
294/// # Errors
295/// [`MaterialiseError`] when a value the output has to state does not
296/// evaluate, a type has no C# shape, or a generated name would collide
297/// with another in its scope — never a silently dropped declaration.
298pub fn csharp_file(ast: &Ast, applied: &Applied) -> Result<csharp::File, MaterialiseError> {
299 let mut root = Scope::default();
300
301 // Structural declarations — the map entry classes — sit at the
302 // global scope and not in a namespace: two identical maps declared
303 // in two modules are one C# class, exactly as two identical
304 // anonymous collections are one C type.
305 let mut structural: Vec<csharp::Item> = Vec::new();
306 let mut emitted: Vec<String> = Vec::new();
307
308 // The same walk the C and Java columns use. C# needs no forward
309 // declarations, so the order is not load-bearing for the compiler;
310 // it is load-bearing for the output, which must not move between two
311 // runs over one model.
312 for id in ordered_types(ast) {
313 let start = match ast.type_(id) {
314 Ok(node) => root.at(ast, node.module)?.items.len(),
315 Err(_) => 0,
316 };
317 declare(ast, applied, id, &mut root, &mut structural, &mut emitted)?;
318 weave_verbatim(ast, id, start, &mut root)?;
319 }
320
321 // Constants and exceptions are not types, so they are walked
322 // separately — the model keeps them in arenas of their own, and
323 // every sibling materialiser does the same.
324 constants(ast, applied, &mut root)?;
325 exceptions(ast, applied, &mut root)?;
326 interfaces(ast, applied, &mut root)?;
327
328 // `@verbatim(placement = BEGIN_FILE)` opens the generated file and
329 // `END_FILE` closes it — OMG IDL 4.2 §8.3.5.1 — before even the
330 // support namespace, because the file and not any scope is what the
331 // placement names.
332 let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::CSHARP);
333 let mut items: Vec<csharp::Item> = begin_file.into_iter().map(csharp::Item::Verbatim).collect();
334 items.push(csharp::Item::Namespace(support_namespace()));
335 items.extend(structural);
336 let (usings, scope_items) = scope_items(root, 0)?;
337 items.extend(scope_items);
338 items.extend(end_file.into_iter().map(csharp::Item::Verbatim));
339 check_names(&items, 0)?;
340
341 Ok(csharp::File {
342 doc: Doc::none(),
343 usings,
344 items,
345 })
346}
347
348/// Weave one type's `@verbatim` text around and into what `declare`
349/// just pushed. OMG IDL 4.2 §8.3.5.1; the slot semantics are
350/// [`crate::verbatim`]'s.
351///
352/// `start` is where the type's items begin in its module's scope.
353/// `BEGIN_DECLARATION` and `END_DECLARATION` land inside the class or
354/// struct body when the type materialised as one; an enum, a `using`
355/// alias or a type that declared nothing has no body this tree can hold
356/// raw text in, so the text lands adjacent to the declaration instead —
357/// the nearest position the tree can express, never a dropped
358/// directive.
359///
360/// # Errors
361/// [`MaterialiseError::NoModule`] when the model does not answer for
362/// the type's module, exactly as `declare` would have reported it.
363fn weave_verbatim(
364 ast: &Ast,
365 id: Id<TypeNode>,
366 start: usize,
367 root: &mut Scope,
368) -> Result<(), MaterialiseError> {
369 let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::CSHARP);
370 if slots.before.is_empty()
371 && slots.begin.is_empty()
372 && slots.end.is_empty()
373 && slots.after.is_empty()
374 {
375 return Ok(());
376 }
377 let Ok(node) = ast.type_(id) else {
378 return Ok(());
379 };
380 let name = csharp::escape_keyword(&node.name);
381 let scope = root.at(ast, node.module)?;
382 let mut declared = scope.items.split_off(start);
383 let mut begin = slots.begin;
384 let mut end = slots.end;
385 let body = declared.iter_mut().find_map(|item| match item {
386 csharp::Item::Type(csharp::TypeDecl::Class(class)) if class.name.as_str() == name => {
387 Some(&mut class.members)
388 }
389 csharp::Item::Type(csharp::TypeDecl::Struct(node)) if node.name.as_str() == name => {
390 Some(&mut node.members)
391 }
392 _ => None,
393 });
394 if let Some(members) = body {
395 for text in begin.drain(..).rev() {
396 members.insert(0, csharp::Member::Verbatim(text));
397 }
398 members.extend(end.drain(..).map(csharp::Member::Verbatim));
399 }
400 scope.items.extend(
401 slots
402 .before
403 .into_iter()
404 .chain(begin)
405 .map(csharp::Item::Verbatim),
406 );
407 scope.items.append(&mut declared);
408 scope.items.extend(
409 end.into_iter()
410 .chain(slots.after)
411 .map(csharp::Item::Verbatim),
412 );
413 Ok(())
414}
415
416// -- the support namespace ------------------------------------------------
417
418/// `namespace ZeroDds.Idl { … }` — the attribute and the `long double`
419/// holder every generated file carries.
420fn support_namespace() -> csharp::Namespace {
421 csharp::Namespace {
422 doc: Doc::lines(&[
423 "What the generated declarations below are written against.",
424 "It is emitted into the file rather than referenced from an assembly:",
425 "one generated file is one compiler argument, and a column that only",
426 "built against a package nobody has written would be a claim.",
427 ]),
428 name: Path::new(SUPPORT_NAMESPACE),
429 usings: Vec::new(),
430 items: vec![
431 csharp::Item::Type(csharp::TypeDecl::Class(idl_name_attribute())),
432 csharp::Item::Type(csharp::TypeDecl::Struct(float128_struct())),
433 ],
434 }
435}
436
437/// The `IdlName` attribute declaration.
438fn idl_name_attribute() -> csharp::Class {
439 let name_property = csharp::Member::Property(csharp::Property {
440 doc: Doc::line("The identifier as the IDL source spelled it."),
441 attributes: Vec::new(),
442 modifiers: vec![csharp::Modifier::Public],
443 type_: csharp::Type::Predefined(csharp::Predefined::String),
444 name: Ident::new("Name"),
445 accessors: vec![csharp::Accessor {
446 kind: csharp::AccessorKind::Get,
447 modifiers: Vec::new(),
448 body: None,
449 expression: None,
450 }],
451 expression: None,
452 initialiser: None,
453 });
454 let constructor = csharp::Member::Constructor(csharp::Constructor {
455 doc: Doc::none(),
456 attributes: Vec::new(),
457 modifiers: vec![csharp::Modifier::Public],
458 name: Ident::new(&idl_name_class()),
459 params: vec![csharp::Param {
460 attributes: Vec::new(),
461 modifier: None,
462 type_: csharp::Type::Predefined(csharp::Predefined::String),
463 name: Ident::new("name"),
464 default: None,
465 }],
466 initialiser: None,
467 body: csharp::Block {
468 statements: vec![csharp::Statement::Expr(csharp::Expr::Assign {
469 target: Box::new(csharp::Expr::Path(Path::single("Name"))),
470 value: Box::new(csharp::Expr::Path(Path::single("name"))),
471 })],
472 },
473 });
474 csharp::Class {
475 doc: Doc::lines(&[
476 "The IDL spelling of the member it annotates.",
477 "",
478 "Load-bearing, not documentation. A member's ID under @autoid(HASH)",
479 "and under @hashid is the first four octets of the MD5 of its IDL",
480 "name (XTypes 1.3 7.3.1.2.1.1), so a field the mapping renamed to",
481 "PascalCase cannot compute its own ID without this.",
482 ]),
483 attributes: Vec::new(),
484 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Sealed],
485 name: Ident::new(&idl_name_class()),
486 generics: Vec::new(),
487 bases: vec![csharp::Type::named(Path::rooted(&["System", "Attribute"]))],
488 constraints: Vec::new(),
489 members: vec![constructor, name_property],
490 }
491}
492
493/// The attribute class's name; C# lets a use drop the `Attribute` suffix.
494fn idl_name_class() -> String {
495 format!("{IDL_NAME_ATTRIBUTE}Attribute")
496}
497
498/// The `long double` holder.
499fn float128_struct() -> csharp::Struct {
500 let field = |name: &str, doc: Doc| {
501 csharp::Member::Field(csharp::Field {
502 doc,
503 attributes: Vec::new(),
504 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Readonly],
505 type_: csharp::Type::Predefined(csharp::Predefined::ULong),
506 name: Ident::new(name),
507 value: None,
508 })
509 };
510 let constructor = csharp::Member::Constructor(csharp::Constructor {
511 doc: Doc::none(),
512 attributes: Vec::new(),
513 modifiers: vec![csharp::Modifier::Public],
514 name: Ident::new(FLOAT128),
515 params: vec![
516 csharp::Param {
517 attributes: Vec::new(),
518 modifier: None,
519 type_: csharp::Type::Predefined(csharp::Predefined::ULong),
520 name: Ident::new("low"),
521 default: None,
522 },
523 csharp::Param {
524 attributes: Vec::new(),
525 modifier: None,
526 type_: csharp::Type::Predefined(csharp::Predefined::ULong),
527 name: Ident::new("high"),
528 default: None,
529 },
530 ],
531 initialiser: None,
532 body: csharp::Block {
533 statements: vec![
534 csharp::Statement::Expr(csharp::Expr::Assign {
535 target: Box::new(csharp::Expr::Path(Path::single("Low"))),
536 value: Box::new(csharp::Expr::Path(Path::single("low"))),
537 }),
538 csharp::Statement::Expr(csharp::Expr::Assign {
539 target: Box::new(csharp::Expr::Path(Path::single("High"))),
540 value: Box::new(csharp::Expr::Path(Path::single("high"))),
541 }),
542 ],
543 },
544 });
545 csharp::Struct {
546 doc: Doc::lines(&[
547 "IEEE 754 binary128 - the sixteen octets CDR gives a `long double`.",
548 "",
549 ".NET has no 128-bit floating type. `decimal` is not one: it is a",
550 "base-ten type with 28 to 29 significant digits and a scale, and a",
551 "member typed against it would put a different number on the wire",
552 "than the IDL wrote. What is held here is the pattern - one sign",
553 "bit, a fifteen-bit exponent biased by 16383 and a 112-bit stored",
554 "fraction (CORBA 3.3 part 2 9.3.1.3, XTypes 1.3 Table 31) - split",
555 "into its low and high halves.",
556 "",
557 "A `struct` where every other generated aggregate is a `class`,",
558 "because this is a primitive: fixed at sixteen octets, never absent",
559 "and never recursive. Arithmetic is not here; the counterparts in",
560 "crates/float128 and runtime/java hold the same pattern.",
561 ]),
562 attributes: Vec::new(),
563 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Readonly],
564 name: Ident::new(FLOAT128),
565 generics: Vec::new(),
566 bases: Vec::new(),
567 constraints: Vec::new(),
568 members: vec![
569 field("Low", Doc::line("The low 64 bits of the pattern.")),
570 field(
571 "High",
572 Doc::line("The high 64 bits: sign, exponent, top fraction."),
573 ),
574 constructor,
575 ],
576 }
577}
578
579// -- the scope tree -------------------------------------------------------
580
581/// One nesting level of the output: a module's declarations, the `using`
582/// aliases it carries, and the modules declared inside it.
583///
584/// A tree and not a list of chains. C# lets a namespace be reopened, so
585/// two blocks of one name would compile — and would put
586/// `module common { struct Robot; }` and
587/// `module common { module util { … } }` in two places a reader has to
588/// join up. One block per module, in first-seen order, is what makes two
589/// runs over one model produce one file.
590#[derive(Default)]
591struct Scope {
592 /// `using` aliases declared in this scope, in declaration order.
593 usings: Vec<csharp::Using>,
594 /// What this module declares directly, in declaration order.
595 items: Vec<csharp::Item>,
596 /// The constants declared in it, which become one class.
597 constants: Vec<csharp::Member>,
598 /// The modules declared inside it, in first-seen order.
599 children: Vec<(Id<ModuleNode>, String, Scope)>,
600}
601
602impl Scope {
603 /// The scope a module names, creating it on the way down.
604 ///
605 /// # Errors
606 /// [`MaterialiseError::NoModule`] when the model does not answer for
607 /// a module in the chain.
608 fn at(&mut self, ast: &Ast, module: Id<ModuleNode>) -> Result<&mut Self, MaterialiseError> {
609 let chain = module_chain(ast, module)?;
610 let mut scope = self;
611 for (id, name) in chain {
612 let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
613 Some(at) => at,
614 None => {
615 scope.children.push((id, name, Self::default()));
616 scope.children.len() - 1
617 }
618 };
619 scope = &mut scope
620 .children
621 .get_mut(at)
622 .ok_or_else(|| MaterialiseError::NoModule {
623 at: module_handle(module),
624 })?
625 .2;
626 }
627 Ok(scope)
628 }
629}
630
631/// A scope's `using` directives and items, with each module inside it as
632/// a nested namespace.
633///
634/// # Errors
635/// [`MaterialiseError::NoModule`] when the nesting runs past
636/// [`MAX_MODULE_DEPTH`], which [`Scope::at`] has already bounded.
637///
638/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
639fn scope_items(
640 scope: Scope,
641 depth: usize,
642) -> Result<(Vec<csharp::Using>, Vec<csharp::Item>), MaterialiseError> {
643 if depth > MAX_MODULE_DEPTH {
644 return Err(MaterialiseError::NoModule {
645 at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
646 });
647 }
648 let mut items = scope.items;
649 if !scope.constants.is_empty() {
650 items.push(csharp::Item::Type(csharp::TypeDecl::Class(csharp::Class {
651 doc: Doc::line(
652 "The constants this scope declares. C# has no namespace-level \
653 field, so a constant needs a type to sit in; the names inside \
654 are the IDL's own.",
655 ),
656 attributes: Vec::new(),
657 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Static],
658 name: Ident::new(CONSTANTS_CLASS),
659 generics: Vec::new(),
660 bases: Vec::new(),
661 constraints: Vec::new(),
662 members: scope.constants,
663 })));
664 }
665 for (_, name, child) in scope.children {
666 let (usings, nested) = scope_items(child, depth + 1)?;
667 items.push(csharp::Item::Namespace(csharp::Namespace {
668 doc: Doc::line("An IDL module, as the namespace its declarations sit in."),
669 name: Path::single(&name),
670 usings,
671 items: nested,
672 }));
673 }
674 Ok((scope.usings, items))
675}
676
677/// The modules a declaration nests in, outermost first, with the
678/// namespace name each takes.
679///
680/// The root module is the anonymous global scope and is not a namespace.
681///
682/// # Errors
683/// [`MaterialiseError::NoModule`] when the model does not answer for a
684/// module, or the parent chain runs past [`MAX_MODULE_DEPTH`]. Returning
685/// the declaration unnested instead would put it in another scope and
686/// say nothing, which is the same class of substitution as writing the
687/// wrong octet.
688fn module_chain(
689 ast: &Ast,
690 module: Id<ModuleNode>,
691) -> Result<Vec<(Id<ModuleNode>, String)>, MaterialiseError> {
692 let mut out = Vec::new();
693 let mut current = Some(module);
694 let mut depth = 0;
695 while let Some(id) = current {
696 if depth > MAX_MODULE_DEPTH {
697 return Err(MaterialiseError::NoModule {
698 at: format!(
699 "{} — the parent chain runs past {MAX_MODULE_DEPTH}",
700 module_handle(module)
701 ),
702 });
703 }
704 let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
705 at: module_handle(id),
706 })?;
707 // The root has no name and no parent, and produces no namespace.
708 if node.parent.is_none() {
709 break;
710 }
711 out.push((id, csharp::escape_keyword(&node.name)));
712 current = node.parent;
713 depth += 1;
714 }
715 out.reverse();
716 Ok(out)
717}
718
719/// The module handle, named without the node the model would not answer
720/// for. Slot and generation are what the arena was asked for.
721fn module_handle(module: Id<ModuleNode>) -> String {
722 format!("#{}@{}", module.slot(), module.generation())
723}
724
725/// Refuse a declaration whose name a C# compiler would reject.
726///
727/// Two collisions, both of which produce a file no compiler reads: two
728/// declarations of one name in one scope (CS0101), and a global-scope
729/// name that would capture the first segment of every rooted reference
730/// this file writes.
731///
732/// zerodds-lint: recursion-depth 64 (namespace nesting; bounded by `MAX_MODULE_DEPTH`)
733fn check_names(items: &[csharp::Item], depth: usize) -> Result<(), MaterialiseError> {
734 if depth > MAX_MODULE_DEPTH {
735 return Err(MaterialiseError::NoModule {
736 at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
737 });
738 }
739 let mut seen: Vec<String> = Vec::new();
740 for item in items {
741 let name = match item {
742 csharp::Item::Namespace(node) => node
743 .name
744 .last()
745 .map(|last| last.as_str().to_string())
746 .unwrap_or_default(),
747 csharp::Item::Type(node) => node.name().as_str().to_string(),
748 csharp::Item::Comment(_) | csharp::Item::Verbatim(_) => continue,
749 };
750 if depth == 0 && RESERVED_TOP_LEVEL.contains(&name.as_str()) {
751 return Err(MaterialiseError::NoLanguageForm {
752 construct: format!(
753 "a declaration named `{name}` at the global scope, which would \
754 capture the first segment of every `global::` reference this \
755 file writes"
756 ),
757 language: "C#",
758 at: name,
759 });
760 }
761 if seen.contains(&name) {
762 return Err(MaterialiseError::NoLanguageForm {
763 construct: format!("a second declaration named `{name}` in one scope"),
764 language: "C#",
765 at: name,
766 });
767 }
768 seen.push(name);
769 if let csharp::Item::Namespace(node) = item {
770 check_names(&node.items, depth + 1)?;
771 }
772 }
773 Ok(())
774}
775
776// -- the declarations -----------------------------------------------------
777
778/// One type's C# declarations, appended where they belong.
779#[allow(clippy::too_many_lines)]
780fn declare(
781 ast: &Ast,
782 applied: &Applied,
783 id: Id<TypeNode>,
784 root: &mut Scope,
785 structural: &mut Vec<csharp::Item>,
786 emitted: &mut Vec<String>,
787) -> Result<(), MaterialiseError> {
788 let Ok(node) = ast.type_(id) else {
789 return Ok(());
790 };
791 match &node.kind {
792 // A primitive, a string and a `fixed` are spellings C# already
793 // has; a sequence is `T[]`. None of them declares anything.
794 TypeKind::Primitive(_)
795 | TypeKind::String { .. }
796 | TypeKind::Fixed { .. }
797 | TypeKind::Sequence { .. } => {}
798
799 // Neither does an array — but every dimension is evaluated
800 // anyway. A C# array type does not state a length, so nothing
801 // here would substitute a zero; what a dimension that does not
802 // evaluate means is that no caller can allocate the array, and a
803 // file that compiles while the length is unknown is exactly the
804 // silent success this project refuses. The C and Java columns
805 // fail on the same input, and the three columns' failure sets
806 // stay comparable.
807 TypeKind::Array { dims, .. } => {
808 for dim in dims {
809 ast.eval_integral(*dim)
810 .map_err(|_| MaterialiseError::Unevaluated {
811 construct: "array length".to_string(),
812 at: named(&node.name, node.node_id),
813 })?;
814 }
815 }
816
817 TypeKind::Map { key, value, .. } => {
818 let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
819 return Err(MaterialiseError::Unevaluated {
820 construct: "map key or value".to_string(),
821 at: named(&node.name, node.node_id),
822 });
823 };
824 let name = map_entry_name(ast, id)?;
825 if emitted.contains(&name) {
826 return Ok(());
827 }
828 emitted.push(name.clone());
829 let at = named(&node.name, node.node_id);
830 let members = fields_of(
831 &name,
832 vec![
833 PendingField {
834 idl_name: "key".to_string(),
835 type_: csharp_member_type(ast, key, &at)?,
836 doc: Doc::none(),
837 },
838 PendingField {
839 idl_name: "value".to_string(),
840 type_: csharp_member_type(ast, value, &at)?,
841 doc: Doc::none(),
842 },
843 ],
844 )?;
845 structural.push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
846 &name,
847 members,
848 Doc::line(
849 "One entry of a map. XTypes 1.3 7.4.1.1.4 defines a map as a \
850 sequence of these, and the rules write the two halves with no \
851 framing between them. An array of these and not a \
852 Dictionary<K,V>: the entry order is the sample's, and it is \
853 what the MapEntryOrder axis measures.",
854 ),
855 ))));
856 }
857
858 // A typedef becomes a `using` alias — the form C# has and Java
859 // does not. The alias is for the reader; every use takes the
860 // resolved type, because a `using` alias may not be built on
861 // another one in the same scope.
862 TypeKind::Alias { aliased } => {
863 if node.name.is_empty() {
864 return Ok(());
865 }
866 if ghost_created_for(applied, node.node_id, "CSharpAlias").is_none() {
867 return Ok(());
868 }
869 let Some(target) = aliased.resolved() else {
870 return Err(MaterialiseError::Unevaluated {
871 construct: "typedef target".to_string(),
872 at: node.name.clone(),
873 });
874 };
875 let type_ = csharp_member_type(ast, target, &node.name)?;
876 root.at(ast, node.module)?.usings.push(csharp::Using {
877 doc: Doc::none(),
878 alias: Some(Ident::new(&csharp::escape_keyword(&node.name))),
879 static_: false,
880 global: false,
881 target: type_,
882 });
883 }
884
885 // A native type is opaque: the mapping is the facet's spelling
886 // and never a guess at what the name might mean. It takes an
887 // alias for the same reason a typedef does.
888 TypeKind::Native => {
889 if ghost_created_for(applied, node.node_id, "CSharpNative").is_none() {
890 return Ok(());
891 }
892 if node.name.is_empty() {
893 return Ok(());
894 }
895 root.at(ast, node.module)?.usings.push(csharp::Using {
896 doc: Doc::line("An IDL `native` type: opaque, mapped by its facet."),
897 alias: Some(Ident::new(&csharp::escape_keyword(&node.name))),
898 static_: false,
899 global: false,
900 target: native_type(ast, id),
901 });
902 }
903
904 TypeKind::Enumeration { enumerators } => {
905 if ghost_created_for(applied, node.node_id, "CSharpEnum").is_none() {
906 return Ok(());
907 }
908 let mut members = Vec::new();
909 let mut taken = vec![csharp::escape_keyword(&node.name)];
910 for enumerator in enumerators {
911 let Ok(enum_node) = ast.enumerator(*enumerator) else {
912 return Err(MaterialiseError::Unevaluated {
913 construct: "enumerator the model listed".to_string(),
914 at: node.name.clone(),
915 });
916 };
917 // Loud, where the Java column skips. Every rule in the
918 // catalogue is unconditional — `part_of` carries no
919 // predicate — so a part the rules did not create is a
920 // defect in the engine or the catalogue and never a
921 // mapping decision. Skipping it drops an enumerator from
922 // the C# enum and leaves a file that still compiles.
923 if ghost_created_for(applied, enum_node.node_id, "CSharpEnumMember").is_none() {
924 return Err(MaterialiseError::Unevaluated {
925 construct: "enumerator, which no rule created a member for".to_string(),
926 at: enum_node.name.clone(),
927 });
928 }
929 let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
930 MaterialiseError::Unevaluated {
931 construct: "enumerator value".to_string(),
932 at: enum_node.name.clone(),
933 }
934 })?;
935 let value = in_range(
936 value,
937 i128::from(i32::MIN),
938 i128::from(u32::MAX),
939 "int",
940 &enum_node.name,
941 )?;
942 // A value above `int`'s range is written as the pattern
943 // it will occupy: XTypes 1.3 7.4.3.5.3 makes an
944 // enumerated literal 32 bits wide and a `@bit_bound(32)`
945 // enumeration may use all of them. `in_range` above has
946 // already refused anything outside those 32 bits.
947 let literal = Literal::int(match i32::try_from(value) {
948 Ok(fits) => i128::from(fits),
949 Err(_) => value - (i128::from(u32::MAX) + 1),
950 });
951 let name = unique(&csharp::escape_keyword(&enum_node.name), &mut taken)?;
952 members.push(csharp::EnumMember {
953 doc: Doc::none(),
954 attributes: vec![idl_name(&enum_node.name)],
955 name: Ident::new(&name),
956 value: Some(csharp::Expr::Literal(literal)),
957 });
958 }
959 root.at(ast, node.module)?
960 .items
961 .push(csharp::Item::Type(csharp::TypeDecl::Enum(csharp::Enum {
962 doc: Doc::line(
963 "An enumeration. A real C# enum and not a set of int \
964 constants: a member's identity here is the integer it is \
965 given, so @value(-1) holds, and every value of the \
966 underlying type is a value of this type, so an \
967 @appendable writer that added an enumerator sends \
968 something a field of this type can carry.",
969 ),
970 attributes: vec![idl_name(&node.name)],
971 modifiers: vec![csharp::Modifier::Public],
972 name: Ident::new(&csharp::escape_keyword(&node.name)),
973 // Written out rather than left to the language
974 // default: XTypes 1.3 7.4.3.5.3 makes the literal 32
975 // bits, and a width that reaches the wire should be
976 // stated where a reader can check it.
977 underlying: Some(csharp::Predefined::Int),
978 members,
979 })));
980 }
981
982 TypeKind::Bitmask { bits } => {
983 if ghost_created_for(applied, node.node_id, "CSharpBitmask").is_none() {
984 return Ok(());
985 }
986 let bound = ast.effective_bit_bound(id);
987 let holder = unsigned_holder(bound);
988 let mut members = Vec::new();
989 let mut taken = vec![csharp::escape_keyword(&node.name)];
990 for bit in bits {
991 let Ok(bit_node) = ast.bit_value(*bit) else {
992 return Err(MaterialiseError::Unevaluated {
993 construct: "bitmask bit the model listed".to_string(),
994 at: node.name.clone(),
995 });
996 };
997 // Loud, for the reason the enumerator above gives: a
998 // bit the rules did not create a member for would vanish
999 // from the flags enum and the file would still compile.
1000 if ghost_created_for(applied, bit_node.node_id, "CSharpBitMember").is_none() {
1001 return Err(MaterialiseError::Unevaluated {
1002 construct: "bitmask bit, which no rule created a member for".to_string(),
1003 at: bit_node.name.clone(),
1004 });
1005 }
1006 let position = ast.effective_bit_position(*bit).ok_or_else(|| {
1007 MaterialiseError::Unevaluated {
1008 construct: "bit position".to_string(),
1009 at: bit_node.name.clone(),
1010 }
1011 })?;
1012 // The value and not a `1 << n` expression. A shift is
1013 // typed by its left operand in C#, so `1 << 40` is an
1014 // int shift evaluating to 256 — silently — and writing
1015 // the suffix that avoids it differs per holder. The
1016 // decimal has one meaning under every holder, and the
1017 // position is in the comment beside it.
1018 let value = 1_i128.checked_shl(u32::from(position)).ok_or_else(|| {
1019 MaterialiseError::NoLanguageForm {
1020 construct: format!("a bit at position {position}"),
1021 language: "C#",
1022 at: bit_node.name.clone(),
1023 }
1024 })?;
1025 let name = unique(&csharp::escape_keyword(&bit_node.name), &mut taken)?;
1026 members.push(csharp::EnumMember {
1027 doc: Doc::line(&format!("Bit {position}.")),
1028 attributes: vec![idl_name(&bit_node.name)],
1029 name: Ident::new(&name),
1030 value: Some(csharp::Expr::Literal(Literal::int(value))),
1031 });
1032 }
1033 root.at(ast, node.module)?
1034 .items
1035 .push(csharp::Item::Type(csharp::TypeDecl::Enum(csharp::Enum {
1036 doc: Doc::line(
1037 "A bitmask, as the flags enum its bits name. The underlying \
1038 type is what Table 33 of XTypes 1.3 7.4.1.1.3.2 gives for \
1039 its bound; [Flags] changes how ToString prints a value and \
1040 nothing about the octets.",
1041 ),
1042 attributes: vec![
1043 csharp::Attribute {
1044 target: None,
1045 path: Path::rooted(&["System", "Flags"]),
1046 args: Vec::new(),
1047 },
1048 idl_name(&node.name),
1049 ],
1050 modifiers: vec![csharp::Modifier::Public],
1051 name: Ident::new(&csharp::escape_keyword(&node.name)),
1052 underlying: Some(holder),
1053 members,
1054 })));
1055 }
1056
1057 TypeKind::Bitset { .. } => {
1058 if ghost_created_for(applied, node.node_id, "CSharpBitset").is_none() {
1059 return Ok(());
1060 }
1061 let name = csharp::escape_keyword(&node.name);
1062 let mut pending = Vec::new();
1063 // The base's bitfields are fields of the derived class too:
1064 // an inherited bitset is one flat run.
1065 for bitfield in &ast.all_bitfields_of(id) {
1066 let Ok(field_node) = ast.bitfield(*bitfield) else {
1067 return Err(MaterialiseError::Unevaluated {
1068 construct: "bitset field the model listed".to_string(),
1069 at: node.name.clone(),
1070 });
1071 };
1072 // An anonymous field is padding: it moves the next field
1073 // along and carries no value of its own.
1074 let Some(field_name) = field_node.name.as_ref() else {
1075 continue;
1076 };
1077 // The width's holder, not the declared one:
1078 // `bitfield<1, boolean>` names a range and not a wire
1079 // form, and the C and Java columns take the width for
1080 // the same reason. The packing into the anchor is the
1081 // codec's.
1082 pending.push(PendingField {
1083 idl_name: field_name.clone(),
1084 type_: csharp::Type::Predefined(unsigned_holder(field_node.bits)),
1085 doc: Doc::none(),
1086 });
1087 }
1088 let members = fields_of(&name, pending)?;
1089 root.at(ast, node.module)?
1090 .items
1091 .push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
1092 &name,
1093 members,
1094 Doc::line(
1095 "A bitset. The fields are separate integers here; the \
1096 packing into the anchor is the codec's.",
1097 ),
1098 ))));
1099 }
1100
1101 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
1102 let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
1103 "CSharpValue"
1104 } else {
1105 "CSharpClass"
1106 };
1107 if ghost_created_for(applied, node.node_id, created).is_none() {
1108 return Ok(());
1109 }
1110 let name = csharp::escape_keyword(&node.name);
1111 // IDL4 to C# 1.0 §7.14.1: "An IDL struct that inherits from a base IDL struct,
1112 // shall be declared as a C# public class that extends the class resulting from
1113 // mapping the base IDL struct." The class holds its own members; the wire still
1114 // carries the base's first (XTypes 1.3 §7.4.1.1.5.1), and the codec reaches them
1115 // through the base class.
1116 //
1117 // Every member is emitted, with no per-member rule lookup: a
1118 // member whose lookup missed would vanish from the class and
1119 // the file would still compile. The C and Java columns take
1120 // the same view.
1121 let mut pending = Vec::new();
1122 for member in ast.members_of(id).iter().copied() {
1123 let Ok(member_node) = ast.member(member) else {
1124 return Err(MaterialiseError::Unevaluated {
1125 construct: "member the model listed".to_string(),
1126 at: node.name.clone(),
1127 });
1128 };
1129 let Some(target) = member_node.type_.resolved() else {
1130 return Err(MaterialiseError::Unevaluated {
1131 construct: "member type".to_string(),
1132 at: member_node.name.clone(),
1133 });
1134 };
1135 let type_ = csharp_member_type(ast, target, &member_node.name)?;
1136 if member_node.facets.is_optional() {
1137 pending.push(PendingField {
1138 idl_name: member_node.name.clone(),
1139 type_: optional(ast, target, type_)?,
1140 doc: Doc::line(
1141 "An @optional member: null when absent. In a @mutable \
1142 type an absent member is not written at all; outside \
1143 one its absence is written - a zero is_present byte \
1144 under XCDR2 (rule 20) or a parameter header declaring \
1145 a length of zero under XCDR1 (7.4.1.2).",
1146 ),
1147 });
1148 } else {
1149 pending.push(PendingField {
1150 idl_name: member_node.name.clone(),
1151 type_,
1152 doc: Doc::none(),
1153 });
1154 }
1155 }
1156 let members = fields_of(&name, pending)?;
1157 let mut class = data_class(&name, members, Doc::none());
1158 if let Some(base) = super::c::struct_base(ast, id) {
1159 class.bases.push(csharp_member_type(ast, base, &node.name)?);
1160 }
1161 // A class something derives from cannot be sealed.
1162 if super::java::has_derived(ast, id) {
1163 class
1164 .modifiers
1165 .retain(|modifier| *modifier != csharp::Modifier::Sealed);
1166 }
1167 root.at(ast, node.module)?
1168 .items
1169 .push(csharp::Item::Type(csharp::TypeDecl::Class(class)));
1170 }
1171
1172 TypeKind::Union { discriminator, .. } => {
1173 if ghost_created_for(applied, node.node_id, "CSharpUnion").is_none() {
1174 return Ok(());
1175 }
1176 let Some(disc) = discriminator.resolved() else {
1177 return Err(MaterialiseError::Unevaluated {
1178 construct: "union discriminator".to_string(),
1179 at: node.name.clone(),
1180 });
1181 };
1182 let name = csharp::escape_keyword(&node.name);
1183 // A field per case and not one per type: two cases may carry
1184 // the same type, and a C# class has no untagged overlay to
1185 // put them in. Which field is live is what the discriminator
1186 // says.
1187 let mut pending = vec![PendingField {
1188 idl_name: "_d".to_string(),
1189 type_: csharp_member_type(ast, disc, &node.name)?,
1190 doc: Doc::line(
1191 "The discriminator. Which case field is live is what this says. \
1192 `_d` is the CORBA accessor's name and not an IDL member, so \
1193 the IdlName below carries it rather than a name from the \
1194 source.",
1195 ),
1196 }];
1197 for case in ast.cases_of(id) {
1198 let Ok(case_node) = ast.case(*case) else {
1199 return Err(MaterialiseError::Unevaluated {
1200 construct: "union case the model listed".to_string(),
1201 at: node.name.clone(),
1202 });
1203 };
1204 // Loud, for the reason the enumerator above gives: a
1205 // case the rules did not create a field for would vanish
1206 // from the class, and a decoder switching on the
1207 // discriminator would have nowhere to put the value.
1208 if ghost_created_for(applied, case_node.node_id, "CSharpCase").is_none() {
1209 return Err(MaterialiseError::Unevaluated {
1210 construct: "union case, which no rule created a field for".to_string(),
1211 at: node.name.clone(),
1212 });
1213 }
1214 let Ok(member) = ast.member(case_node.member) else {
1215 return Err(MaterialiseError::Unevaluated {
1216 construct: "member behind a union case the model listed".to_string(),
1217 at: node.name.clone(),
1218 });
1219 };
1220 let Some(target) = member.type_.resolved() else {
1221 return Err(MaterialiseError::Unevaluated {
1222 construct: "union case type".to_string(),
1223 at: member.name.clone(),
1224 });
1225 };
1226 pending.push(PendingField {
1227 idl_name: member.name.clone(),
1228 type_: csharp_member_type(ast, target, &member.name)?,
1229 doc: Doc::none(),
1230 });
1231 }
1232 let members = fields_of(&name, pending)?;
1233 root.at(ast, node.module)?
1234 .items
1235 .push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
1236 &name,
1237 members,
1238 Doc::line("A union: the discriminator, and one field per case."),
1239 ))));
1240 }
1241
1242 // A value box is a reference in C# already: the boxed type, null
1243 // when absent. Nothing to declare.
1244 TypeKind::ValueBox { .. } => {}
1245
1246 // An object reference belongs to the RPC arc and an annotation
1247 // type has no runtime form at all — `zerodds_ast_layout` refuses
1248 // to plan either — so a C# declaration would be a shape nothing
1249 // could serialise. `any` and `ValueBase` are in the model because
1250 // Building Block Any is one of IDL 4.2's fifteen, and what they
1251 // hold is decided at run time. All four are named here rather
1252 // than passed over, and `csharp_member_type` refuses them by name
1253 // where a member would carry one.
1254 TypeKind::ObjectRef { .. }
1255 | TypeKind::AnnotationType { .. }
1256 | TypeKind::Any
1257 | TypeKind::ValueBase => {}
1258 }
1259 Ok(())
1260}
1261
1262/// The IDL constants, as `const` fields of each scope's holder class.
1263///
1264/// # Errors
1265/// As [`csharp_file`].
1266fn constants(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
1267 for (id, node) in ast.constants() {
1268 if ghost_created_for(applied, node.node_id, "CSharpConst").is_none() {
1269 continue;
1270 }
1271 let _ = id;
1272 // `eval_const` and not `eval_integral`: `const string MODULE_ID`
1273 // evaluates perfectly well and is not a number, and asking the
1274 // integral question about it answered `Unevaluated` and dropped
1275 // the field.
1276 let value = ast
1277 .eval_const(node.expr)
1278 .map_err(|_| MaterialiseError::Unevaluated {
1279 construct: "constant's value".to_string(),
1280 at: node.name.clone(),
1281 })?;
1282 let type_ = match node.type_.resolved() {
1283 Some(target) => csharp_member_type(ast, target, &node.name)?,
1284 None => csharp::Type::Predefined(csharp::Predefined::Int),
1285 };
1286 let value = constant_expr(&type_, &value, &node.name)?;
1287 // The name is the IDL's, not folded: a constant is not a member,
1288 // nothing computes an ID from it, and SCREAMING_CASE is what IDL
1289 // constants are written in and what .NET leaves alone.
1290 let name = csharp::escape_keyword(&node.name);
1291 root.at(ast, node.module)?
1292 .constants
1293 .push(csharp::Member::Field(csharp::Field {
1294 doc: Doc::none(),
1295 attributes: vec![idl_name(&node.name)],
1296 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Const],
1297 type_,
1298 name: Ident::new(&name),
1299 value: Some(value),
1300 }));
1301 }
1302 Ok(())
1303}
1304
1305/// The IDL exceptions, as data classes.
1306///
1307/// A holder and **not** a `System.Exception`. An IDL exception member
1308/// named `message`, `data` or `source` folds to `Message`, `Data` or
1309/// `Source`, which are `System.Exception`'s own members — the generated
1310/// field would hide an inherited one (CS0108) or fail to override a
1311/// property that has no setter, and nothing about the wire form decides
1312/// which of the two the peer meant. The Java column reaches the same
1313/// place from `Serializable` and `serialVersionUID`.
1314///
1315/// # Errors
1316/// As [`csharp_file`].
1317fn exceptions(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
1318 for (id, node) in ast.exceptions() {
1319 if ghost_created_for(applied, node.node_id, "CSharpError").is_none() {
1320 continue;
1321 }
1322 let _ = id;
1323 let name = csharp::escape_keyword(&node.name);
1324 let mut pending = Vec::new();
1325 for member in &node.members {
1326 let Ok(member_node) = ast.member(*member) else {
1327 return Err(MaterialiseError::Unevaluated {
1328 construct: "exception member the model listed".to_string(),
1329 at: node.name.clone(),
1330 });
1331 };
1332 let Some(target) = member_node.type_.resolved() else {
1333 return Err(MaterialiseError::Unevaluated {
1334 construct: "exception member type".to_string(),
1335 at: member_node.name.clone(),
1336 });
1337 };
1338 pending.push(PendingField {
1339 idl_name: member_node.name.clone(),
1340 type_: csharp_member_type(ast, target, &member_node.name)?,
1341 doc: Doc::none(),
1342 });
1343 }
1344 let members = fields_of(&name, pending)?;
1345 root.at(ast, node.module)?
1346 .items
1347 .push(csharp::Item::Type(csharp::TypeDecl::Class(data_class(
1348 &name,
1349 members,
1350 Doc::lines(&[
1351 "An exception, as the structure its members make.",
1352 "",
1353 "A data holder and not a System.Exception: an IDL member named",
1354 "`message` or `data` folds onto one of System.Exception's own",
1355 "members, and hiding an inherited member (CS0108) is not a",
1356 "decision the wire form makes.",
1357 ]),
1358 ))));
1359 }
1360 Ok(())
1361}
1362
1363/// One `adapter::interface::Parameter` as a C# `Param`.
1364fn csharp_param(
1365 ast: &Ast,
1366 param: &interface::Parameter,
1367) -> Result<csharp::Param, MaterialiseError> {
1368 Ok(csharp::Param {
1369 // No IdlName here. A parameter's name is not folded, so nothing
1370 // about it is at risk; an attribute that repeated the name it
1371 // already carries would state nothing.
1372 attributes: Vec::new(),
1373 modifier: match param.direction {
1374 zerodds_ast::ParamDirection::In => None,
1375 zerodds_ast::ParamDirection::Out => Some(csharp::ParamModifier::Out),
1376 zerodds_ast::ParamDirection::InOut => Some(csharp::ParamModifier::Ref),
1377 },
1378 type_: csharp_member_type(ast, param.type_, ¶m.name)?,
1379 // Verbatim: a parameter name is not a member, nothing computes
1380 // an ID from it, and IDL already writes parameters in the lower
1381 // camel case C# asks for.
1382 name: Ident::new(&csharp::escape_keyword(¶m.name)),
1383 default: None,
1384 })
1385}
1386
1387/// The IDL interfaces, as C# interfaces of operation signatures and
1388/// properties.
1389///
1390/// The shape the IDL declared and nothing else: the transport belongs to
1391/// the RPC arc, exactly as it does in the Rust column, whose
1392/// `interface-to-trait` produces a trait of signatures with no bodies.
1393/// Materialised rather than left to the rule alone — a catalogue rule
1394/// whose created node nothing reads is a rule claiming a coverage cell
1395/// that produces no output, and that is a claim rather than a mapping.
1396///
1397/// The name is verbatim and carries no `I` prefix. .NET convention asks
1398/// for one, and an IDL interface's name reaches the wire in its CORBA
1399/// repository ID — `IDL:Robot:1.0` — so `IRobot` would be a type nothing
1400/// resolves back.
1401///
1402/// A direction becomes the C# passing mode that means the same thing:
1403/// `out` is `out` and `inout` is `ref`. `in` is the default and is
1404/// written as nothing, which is what C# means by it.
1405///
1406/// An IDL `attribute` becomes a C# property — OMG's IDL-to-C# mapping's
1407/// own idiom for it, and the one shape here with no operation-pair
1408/// alternative to weigh against it. `readonly` writes a getter and no
1409/// setter; both directions' `getraises`/`setraises` are documented, not
1410/// enforced — C# has no checked exception, the same reason `raises` on
1411/// an operation is documented and not a `throws` clause.
1412///
1413/// A derived interface is flattened: the emitted interface has no base
1414/// list, so OMG IDL 4.2 §7.4.3's inherited operations and attributes are
1415/// written into it directly.
1416/// [`interface::all_operations_flattened`]/[`interface::
1417/// all_attributes_flattened`] are what answer with them, bases before
1418/// self, matching [`Ast::all_operations_of`]'s own order.
1419///
1420/// # Errors
1421/// As [`csharp_file`].
1422fn interfaces(ast: &Ast, applied: &Applied, root: &mut Scope) -> Result<(), MaterialiseError> {
1423 for (id, node) in ast.interfaces() {
1424 if ghost_created_for(applied, node.node_id, "CSharpInterface").is_none() {
1425 continue;
1426 }
1427 let name = csharp::escape_keyword(&node.name);
1428 let mut members = Vec::new();
1429 let mut taken = vec![name.clone()];
1430
1431 let operations = interface::all_operations_flattened(ast, id)
1432 .map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
1433 for op in operations
1434 .iter()
1435 .filter(|op| op.origin == interface::Origin::Declared)
1436 {
1437 let mut params = Vec::new();
1438 for param in &op.params {
1439 params.push(csharp_param(ast, param)?);
1440 }
1441 let returns = match op.returns {
1442 None => csharp::Type::Predefined(csharp::Predefined::Void),
1443 Some(target) => csharp_member_type(ast, target, &op.name)?,
1444 };
1445 let method = unique(&csharp::escape_keyword(&capitalise(&op.name)), &mut taken)?;
1446 let doc = op
1447 .raises_line()
1448 .map_or_else(Doc::none, |line| Doc::line(&line));
1449 members.push(csharp::Member::Method(csharp::Method {
1450 doc,
1451 attributes: vec![idl_name(&op.name)],
1452 // No modifier: an interface member is public and
1453 // abstract by definition, and writing either is a
1454 // compile error before C# 8 and noise after it.
1455 modifiers: Vec::new(),
1456 returns,
1457 name: Ident::new(&method),
1458 generics: Vec::new(),
1459 params,
1460 constraints: Vec::new(),
1461 body: None,
1462 expression: None,
1463 }));
1464 }
1465
1466 let attributes = interface::all_attributes_flattened(ast, id)
1467 .map_err(|error| interface::interface_error_to_materialise(error, &node.name))?;
1468 for attribute in &attributes {
1469 let type_ = csharp_member_type(ast, attribute.type_, &attribute.name)?;
1470 let property = unique(
1471 &csharp::escape_keyword(&capitalise(&attribute.name)),
1472 &mut taken,
1473 )?;
1474 let mut doc = Doc::none();
1475 if let Some(line) = attribute.get_raises_line() {
1476 doc.push(&format!("Getter: {line}"));
1477 }
1478 if let Some(line) = attribute.set_raises_line() {
1479 doc.push(&format!("Setter: {line}"));
1480 }
1481 let mut accessors = vec![csharp::Accessor {
1482 kind: csharp::AccessorKind::Get,
1483 modifiers: Vec::new(),
1484 body: None,
1485 expression: None,
1486 }];
1487 if !attribute.readonly {
1488 accessors.push(csharp::Accessor {
1489 kind: csharp::AccessorKind::Set,
1490 modifiers: Vec::new(),
1491 body: None,
1492 expression: None,
1493 });
1494 }
1495 members.push(csharp::Member::Property(csharp::Property {
1496 doc,
1497 attributes: vec![idl_name(&attribute.name)],
1498 modifiers: Vec::new(),
1499 type_,
1500 name: Ident::new(&property),
1501 accessors,
1502 expression: None,
1503 initialiser: None,
1504 }));
1505 }
1506
1507 root.at(ast, node.module)?
1508 .items
1509 .push(csharp::Item::Type(csharp::TypeDecl::Interface(
1510 csharp::Interface {
1511 doc: Doc::lines(&[
1512 "An interface, as the operation signatures and attribute",
1513 "properties it has.",
1514 "",
1515 "The shape and not the transport: how a call is carried is the RPC",
1516 "arc's, and the Rust column's `interface-to-trait` stops in the same",
1517 "place. `raises`/`getraises`/`setraises` have no C# clause - the",
1518 "language has no checked exception - and are documented on each",
1519 "member instead; `oneway` is a transport property, so it is not",
1520 "written here either.",
1521 "",
1522 "A derived interface carries its bases' operations and attributes",
1523 "too: this declaration has no base list, so IDL 4.2 7.4.3's",
1524 "inherited members are written into it rather than reached through",
1525 "one.",
1526 ]),
1527 attributes: vec![idl_name(&node.name)],
1528 modifiers: vec![csharp::Modifier::Public],
1529 name: Ident::new(&name),
1530 generics: Vec::new(),
1531 bases: Vec::new(),
1532 constraints: Vec::new(),
1533 members,
1534 },
1535 )));
1536 }
1537 Ok(())
1538}
1539
1540// -- fields ---------------------------------------------------------------
1541
1542/// One field, before its C# name has been settled against the others.
1543struct PendingField {
1544 /// The IDL spelling, which the attribute carries.
1545 idl_name: String,
1546 /// Its type.
1547 type_: csharp::Type,
1548 /// Its documentation.
1549 doc: Doc,
1550}
1551
1552/// The members of a type, with every C# name settled.
1553///
1554/// The fold is `venue` to `Venue`; where that collides with the type's
1555/// own name — CS0542 forbids a member sharing it — or with a field
1556/// already placed, an underscore is appended until it does not. Neither
1557/// move loses anything: the `[IdlName]` attribute on every field carries
1558/// the IDL spelling verbatim, which is the only spelling a hashed member
1559/// ID can be computed from.
1560///
1561/// # Errors
1562/// [`MaterialiseError::NoLanguageForm`] when a name will not come apart
1563/// within [`MAX_RENAME_ATTEMPTS`].
1564fn fields_of(
1565 type_name: &str,
1566 pending: Vec<PendingField>,
1567) -> Result<Vec<csharp::Member>, MaterialiseError> {
1568 let mut taken = vec![type_name.to_string()];
1569 let mut out = Vec::new();
1570 for field in pending {
1571 let folded = field_name(&field.idl_name);
1572 let name = unique(&folded, &mut taken)?;
1573 out.push(csharp::Member::Field(csharp::Field {
1574 doc: field.doc,
1575 attributes: vec![idl_name(&field.idl_name)],
1576 modifiers: vec![csharp::Modifier::Public],
1577 type_: field.type_,
1578 name: Ident::new(&name),
1579 value: None,
1580 }));
1581 }
1582 Ok(out)
1583}
1584
1585/// `name`, suffixed until it is not in `taken`, and recorded there.
1586///
1587/// # Errors
1588/// [`MaterialiseError::NoLanguageForm`] when [`MAX_RENAME_ATTEMPTS`] is
1589/// reached. A bound and not a `loop`: an unbounded rename would hang on
1590/// a model that somehow produced them all, and hanging says nothing.
1591fn unique(name: &str, taken: &mut Vec<String>) -> Result<String, MaterialiseError> {
1592 let mut candidate = name.to_string();
1593 for _ in 0..MAX_RENAME_ATTEMPTS {
1594 if !taken.contains(&candidate) {
1595 taken.push(candidate.clone());
1596 return Ok(candidate);
1597 }
1598 candidate.push('_');
1599 }
1600 Err(MaterialiseError::NoLanguageForm {
1601 construct: format!(
1602 "the name `{name}`, which will not come apart from the others in its \
1603 scope within {MAX_RENAME_ATTEMPTS} attempts"
1604 ),
1605 language: "C#",
1606 at: name.to_string(),
1607 })
1608}
1609
1610/// The `[global::ZeroDds.Idl.IdlName("…")]` attribute.
1611fn idl_name(name: &str) -> csharp::Attribute {
1612 let mut segments: Vec<&str> = SUPPORT_NAMESPACE.to_vec();
1613 segments.push(IDL_NAME_ATTRIBUTE);
1614 csharp::Attribute {
1615 target: None,
1616 path: Path::rooted(&segments),
1617 args: vec![csharp::AttributeArg::Positional(csharp::Expr::Literal(
1618 Literal::string(name),
1619 ))],
1620 }
1621}
1622
1623/// The C# name a member, a case or a bitfield takes from its IDL name.
1624///
1625/// The one place the fold is written, because two halves of the column
1626/// have to agree on it: this materialiser declares the field and
1627/// `crate::emit::csharp::CSharp::escape` reaches it from the codec. A
1628/// second reading would be a field no compiler finds — loud, but only
1629/// once a case exercises it.
1630///
1631/// What it does **not** carry is the collision rule. `fields_of`
1632/// suffixes a name that would take the enclosing type's (CS0542) or one
1633/// already placed, and it needs the other fields to know; the codec's
1634/// seam is handed a name and nothing else. The consequence is measured
1635/// rather than hidden: a member whose folded name collides produces a
1636/// codec that names the unsuffixed field, `csc` reports CS0117, and the
1637/// case is a refusal with the compiler's own message rather than a
1638/// silently wrong field.
1639#[must_use]
1640pub fn field_name(idl: &str) -> String {
1641 csharp::escape_keyword(&capitalise(idl))
1642}
1643
1644/// Where a named declaration sits, rooted at the global namespace.
1645///
1646/// `global::a.b.Colour`. What a codec writes to reach an enumerator or a
1647/// bitmask constant the materialiser declared; the bare name resolves to
1648/// nothing outside the namespace it was declared in.
1649///
1650/// # Errors
1651/// [`MaterialiseError`] when the model will not answer for the type or
1652/// for a module it nests in.
1653pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
1654 let path = qualified(ast, id)?;
1655 let mut out = String::from("global::");
1656 let mut first = true;
1657 for segment in path.segments() {
1658 if !first {
1659 out.push('.');
1660 }
1661 out.push_str(segment.as_str());
1662 first = false;
1663 }
1664 Ok(out)
1665}
1666
1667/// Take the `ZeroDds.Idl` support namespace off a materialised file.
1668///
1669/// [`csharp_file`] emits `IdlNameAttribute` and `Float128` into every
1670/// file so that one generated source is one compiler argument. A caller
1671/// that compiles the file **with** `runtime/csharp` must not: that
1672/// assembly declares the same two names in the same namespace, and two
1673/// declarations of one name is CS0101.
1674///
1675/// The runtime's versions are a superset — the same two `ulong` fields
1676/// and the same constructor on `Float128`, plus the conversions the wire
1677/// path needs — and `runtime/csharp/tests/SelfTest.cs` holds the
1678/// two together so the superset claim is asserted rather than assumed.
1679///
1680/// Returns whether a namespace was removed, so a caller that expected
1681/// one and found none can say so instead of carrying on.
1682pub fn drop_support_namespace(file: &mut csharp::File) -> bool {
1683 let before = file.items.len();
1684 file.items.retain(|item| match item {
1685 csharp::Item::Namespace(node) => {
1686 node.name.segments().len() != SUPPORT_NAMESPACE.len()
1687 || !node
1688 .name
1689 .segments()
1690 .iter()
1691 .zip(SUPPORT_NAMESPACE)
1692 .all(|(written, wanted)| written.as_str() == *wanted)
1693 }
1694 _ => true,
1695 });
1696 file.items.len() != before
1697}
1698
1699/// `venue` to `Venue` — the first letter and nothing else.
1700///
1701/// The fold a full `PascalCase` would do is not injective: IDL 4.2
1702/// §7.2.3 forbids two identifiers in one scope that differ only in case
1703/// but permits `my_field` beside `myField`, and both fold to `MyField`.
1704/// One of the two members would then disappear from the class, which is
1705/// the failure this whole file is arranged against.
1706fn capitalise(name: &str) -> String {
1707 let mut chars = name.chars();
1708 chars.next().map_or_else(String::new, |first| {
1709 first.to_uppercase().collect::<String>() + chars.as_str()
1710 })
1711}
1712
1713/// `public sealed class <name> { … }`
1714///
1715/// `sealed`, because nothing derives from a generated data class: an
1716/// IDL base's members are folded into the derived type's own field list
1717/// (XTypes 1.3 §7.4.1.1.5.1), so a C# base class would hold a second
1718/// copy of them.
1719fn data_class(name: &str, members: Vec<csharp::Member>, doc: Doc) -> csharp::Class {
1720 csharp::Class {
1721 doc,
1722 attributes: vec![idl_name(name)],
1723 modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Sealed],
1724 name: Ident::new(name),
1725 generics: Vec::new(),
1726 bases: Vec::new(),
1727 constraints: Vec::new(),
1728 members,
1729 }
1730}
1731
1732// -- types ----------------------------------------------------------------
1733
1734/// The C# type of a value in a position that has to hold one, refusing
1735/// the constructs C# cannot hold.
1736///
1737/// `any` is the first of those. What it holds is decided at run time —
1738/// there is no C# type for it and no wire form to generate a codec
1739/// against. Emitting `object a;` into the class and reporting success
1740/// produces a field no encoder can write, from a generator that said
1741/// nothing was wrong.
1742///
1743/// # Errors
1744/// [`MaterialiseError::NoLanguageForm`] for `any`, `ValueBase`, an
1745/// object reference and an annotation type;
1746/// [`MaterialiseError::Unevaluated`] for a type the model will not
1747/// answer for.
1748pub fn csharp_member_type(
1749 ast: &Ast,
1750 target: Id<TypeNode>,
1751 at: &str,
1752) -> Result<csharp::Type, MaterialiseError> {
1753 type_at(ast, target, at, 0)
1754}
1755
1756/// [`csharp_member_type`], carrying how deep the spelling has nested.
1757///
1758/// zerodds-lint: recursion-depth 64 (collection and typedef nesting;
1759/// bounded by `MAX_TYPE_DEPTH`, which returns
1760/// `MaterialiseError::Unevaluated` naming the spelling that nested)
1761fn type_at(
1762 ast: &Ast,
1763 id: Id<TypeNode>,
1764 at: &str,
1765 depth: usize,
1766) -> Result<csharp::Type, MaterialiseError> {
1767 if depth > MAX_TYPE_DEPTH {
1768 return Err(MaterialiseError::Unevaluated {
1769 construct: format!("type, whose C# spelling nests past {MAX_TYPE_DEPTH}"),
1770 at: at.to_string(),
1771 });
1772 }
1773 let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1774 construct: "type".to_string(),
1775 at: at.to_string(),
1776 })?;
1777 let refused = match &node.kind {
1778 TypeKind::Any => Some("any"),
1779 TypeKind::ValueBase => Some("ValueBase"),
1780 // An object reference is a handle into the RPC arc and
1781 // `coverage.rs` says so; there is no C# data type for it.
1782 TypeKind::ObjectRef { .. } => Some("Object"),
1783 // An annotation type has no runtime form, so nothing declares
1784 // one and a reference to it would name a class that is not there.
1785 TypeKind::AnnotationType { .. } => Some("annotation type"),
1786 _ => None,
1787 };
1788 if let Some(construct) = refused {
1789 return Err(MaterialiseError::NoLanguageForm {
1790 construct: construct.to_string(),
1791 language: "C#",
1792 at: at.to_string(),
1793 });
1794 }
1795 Ok(match &node.kind {
1796 // Matched ahead of the arm below, which would reach
1797 // `csharp_primitive`'s `double` — eight octets where the member
1798 // is sixteen.
1799 TypeKind::Primitive(PrimitiveKind::Float128) => {
1800 let mut segments: Vec<&str> = SUPPORT_NAMESPACE.to_vec();
1801 segments.push(FLOAT128);
1802 csharp::Type::named(Path::rooted(&segments))
1803 }
1804 TypeKind::Primitive(kind) => csharp::Type::Predefined(csharp_primitive(*kind)),
1805 // A `wstring` is a `string` like a `string`: .NET strings are
1806 // UTF-16 already, and the surrogate rule of XTypes 1.3
1807 // 7.4.1.1.2 belongs to the codec.
1808 TypeKind::String { .. } | TypeKind::Fixed { .. } => {
1809 csharp::Type::Predefined(csharp::Predefined::String)
1810 }
1811 TypeKind::Sequence { element, .. } => {
1812 let Some(element) = element.resolved() else {
1813 return Err(MaterialiseError::Unevaluated {
1814 construct: "sequence element".to_string(),
1815 at: at.to_string(),
1816 });
1817 };
1818 csharp::Type::array(type_at(ast, element, at, depth + 1)?)
1819 }
1820 TypeKind::Array { element, dims } => {
1821 let Some(element) = element.resolved() else {
1822 return Err(MaterialiseError::Unevaluated {
1823 construct: "array element".to_string(),
1824 at: at.to_string(),
1825 });
1826 };
1827 let mut out = type_at(ast, element, at, depth + 1)?;
1828 // One `[]` per dimension, jagged. A C# rectangular `T[,]`
1829 // would state the rank and still not the lengths, and it
1830 // cannot be sliced into rows the way an encoder walks them;
1831 // the lengths are the plan's, and `declare` evaluates them
1832 // there.
1833 for _ in dims {
1834 out = csharp::Type::array(out);
1835 }
1836 out
1837 }
1838 TypeKind::Map { .. } => {
1839 csharp::Type::array(csharp::Type::named(Path::rooted(&[&map_entry_name(
1840 ast, id,
1841 )?])))
1842 }
1843 // A typedef is transparent: the use takes the target's type. The
1844 // alias `declare` emits is for the reader, and a `using` alias
1845 // may not be built on another one in the same scope.
1846 TypeKind::Alias { aliased } => {
1847 let Some(target) = aliased.resolved() else {
1848 return Err(MaterialiseError::Unevaluated {
1849 construct: "typedef target".to_string(),
1850 at: at.to_string(),
1851 });
1852 };
1853 type_at(ast, target, at, depth + 1)?
1854 }
1855 TypeKind::Structure { .. }
1856 | TypeKind::Union { .. }
1857 | TypeKind::Enumeration { .. }
1858 | TypeKind::Bitmask { .. }
1859 | TypeKind::Bitset { .. }
1860 | TypeKind::Valuetype { .. } => csharp::Type::named(qualified(ast, id)?),
1861 TypeKind::Native => native_type(ast, id),
1862 // A boxed value is a reference, absent when null — which is what
1863 // a C# class reference already is. A value type takes `T?`.
1864 TypeKind::ValueBox { boxed } => {
1865 let Some(target) = boxed.resolved() else {
1866 return Err(MaterialiseError::Unevaluated {
1867 construct: "value box target".to_string(),
1868 at: at.to_string(),
1869 });
1870 };
1871 let inner = type_at(ast, target, at, depth + 1)?;
1872 optional(ast, target, inner)?
1873 }
1874 TypeKind::Any
1875 | TypeKind::ValueBase
1876 | TypeKind::ObjectRef { .. }
1877 | TypeKind::AnnotationType { .. } => {
1878 // Refused above; the arm keeps the match total, so a kind
1879 // added to the model without a C# answer is a compile error
1880 // and not a field that silently takes another's type.
1881 return Err(MaterialiseError::NoLanguageForm {
1882 construct: node.kind.as_str().to_string(),
1883 language: "C#",
1884 at: at.to_string(),
1885 });
1886 }
1887 })
1888}
1889
1890/// The C# keyword type an IDL primitive becomes.
1891///
1892/// Exhaustive, so a primitive added to the model without a C# spelling
1893/// is a compile error and not a member that silently takes another's.
1894///
1895/// `long double` is **not** routed here: it takes the generated
1896/// `Float128`. The arm below exists so the match is total and answers
1897/// for a caller that wants the nearest keyword type; `type_at` matches
1898/// it before it reaches this function.
1899///
1900/// The two rows to read twice are `Int32` and `Int64`. IDL `long` is 32
1901/// bits and C# `long` is 64; a mapping that carried the spelling across
1902/// would compile and write four octets where the peer reads eight.
1903#[must_use]
1904pub const fn csharp_primitive(kind: PrimitiveKind) -> csharp::Predefined {
1905 match kind {
1906 PrimitiveKind::Boolean => csharp::Predefined::Bool,
1907 // A C# `char` is an unsigned 16-bit code unit, which holds an
1908 // IDL `char`'s 0..255 and a `wchar`'s BMP code point alike. The
1909 // wire width is the codec's and is not this type's.
1910 PrimitiveKind::Char | PrimitiveKind::WChar => csharp::Predefined::Char,
1911 PrimitiveKind::Int8 => csharp::Predefined::SByte,
1912 PrimitiveKind::Octet | PrimitiveKind::UInt8 => csharp::Predefined::Byte,
1913 PrimitiveKind::Int16 => csharp::Predefined::Short,
1914 PrimitiveKind::UInt16 => csharp::Predefined::UShort,
1915 // IDL `long` — 32 bits, so `int`.
1916 PrimitiveKind::Int32 => csharp::Predefined::Int,
1917 PrimitiveKind::UInt32 => csharp::Predefined::UInt,
1918 // IDL `long long` — 64 bits, so `long`.
1919 PrimitiveKind::Int64 => csharp::Predefined::Long,
1920 PrimitiveKind::UInt64 => csharp::Predefined::ULong,
1921 PrimitiveKind::Float32 => csharp::Predefined::Float,
1922 PrimitiveKind::Float64 | PrimitiveKind::Float128 => csharp::Predefined::Double,
1923 }
1924}
1925
1926/// The unsigned C# type a value of `bits` bits is held in.
1927///
1928/// Table 33 of XTypes 1.3 §7.4.1.1.3.2, one to one: C# has an unsigned
1929/// keyword type at every width, so nothing has to be widened to stay a
1930/// number and nothing is held as a pattern. This is where the column
1931/// most visibly parts from Java, whose `unsigned_holder` can only answer
1932/// `int` or `long`.
1933#[must_use]
1934pub const fn unsigned_holder(bits: u16) -> csharp::Predefined {
1935 match bits {
1936 0..=8 => csharp::Predefined::Byte,
1937 9..=16 => csharp::Predefined::UShort,
1938 17..=32 => csharp::Predefined::UInt,
1939 _ => csharp::Predefined::ULong,
1940 }
1941}
1942
1943/// The type an `@optional` member of `target` takes.
1944///
1945/// A value type becomes `T?`, which is `Nullable<T>` and a different
1946/// type from `T`. A reference type is returned unchanged: it is nullable
1947/// already, and writing `T?` on one outside a `#nullable enable` context
1948/// is CS8632 — a warning about an annotation the compiler will not act
1949/// on, which is worse than no annotation at all.
1950///
1951/// # Errors
1952/// As [`csharp_member_type`].
1953fn optional(
1954 ast: &Ast,
1955 target: Id<TypeNode>,
1956 type_: csharp::Type,
1957) -> Result<csharp::Type, MaterialiseError> {
1958 if is_value_type(ast, target, 0)? {
1959 return Ok(csharp::Type::Nullable(Box::new(type_)));
1960 }
1961 Ok(type_)
1962}
1963
1964/// Whether an `@optional` member of `target` is declared `T?`.
1965///
1966/// The write half of the mapping this file's fourth decision makes: a
1967/// value type is wrapped in `Nullable<T>` and a reference type is not.
1968/// The codec has to know, because `int?` has no implicit conversion to
1969/// `int` and `string?` has no `.Value` — the two halves cannot be
1970/// spelled the same way, and a backend that guessed would produce a
1971/// file `csc` refuses in one direction and accepts wrongly in neither.
1972///
1973/// A model type the resolver will not answer for is not wrapped, which
1974/// is the same answer `optional` gives it.
1975#[must_use]
1976pub fn is_nullable_wrapped(ast: &Ast, target: Id<TypeNode>) -> bool {
1977 is_value_type(ast, target, 0).unwrap_or(false)
1978}
1979
1980/// Whether the C# type a model type becomes is a value type.
1981///
1982/// Asked of the model and not of the produced [`csharp::Type`]: a
1983/// generated `enum` and a generated `class` are both a
1984/// [`csharp::Type::Named`], and only the model knows which is which.
1985///
1986/// # Errors
1987/// [`MaterialiseError::Unevaluated`] when the chain nests past
1988/// [`MAX_TYPE_DEPTH`].
1989///
1990/// zerodds-lint: recursion-depth 64 (alias chains; bounded by `MAX_TYPE_DEPTH`)
1991fn is_value_type(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<bool, MaterialiseError> {
1992 if depth > MAX_TYPE_DEPTH {
1993 return Err(MaterialiseError::Unevaluated {
1994 construct: format!("type, whose chain nests past {MAX_TYPE_DEPTH}"),
1995 at: "an @optional member".to_string(),
1996 });
1997 }
1998 let Ok(node) = ast.type_(id) else {
1999 return Ok(false);
2000 };
2001 Ok(match &node.kind {
2002 // Every keyword numeric, and the generated `Float128` struct.
2003 TypeKind::Primitive(_) => true,
2004 // Generated `enum`s, which are value types like any other.
2005 TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => true,
2006 TypeKind::Alias { aliased } => match aliased.resolved() {
2007 Some(target) => is_value_type(ast, target, depth + 1)?,
2008 None => false,
2009 },
2010 // `string`, arrays and every generated class. A `native` type is
2011 // here too: its facet may name a value type, but a facet that
2012 // did and a `?` this file added would be a claim about a
2013 // spelling the IDL only handed through.
2014 TypeKind::String { .. }
2015 | TypeKind::Fixed { .. }
2016 | TypeKind::Sequence { .. }
2017 | TypeKind::Array { .. }
2018 | TypeKind::Map { .. }
2019 | TypeKind::Structure { .. }
2020 | TypeKind::Union { .. }
2021 | TypeKind::Bitset { .. }
2022 | TypeKind::Valuetype { .. }
2023 | TypeKind::ValueBox { .. }
2024 | TypeKind::Native
2025 | TypeKind::Any
2026 | TypeKind::ValueBase
2027 | TypeKind::ObjectRef { .. }
2028 | TypeKind::AnnotationType { .. } => false,
2029 })
2030}
2031
2032/// What a `native` declaration maps to.
2033///
2034/// The facet if a `#pragma` or an annotation gave one, and `object`
2035/// otherwise — never a guess at what the name might mean.
2036fn native_type(ast: &Ast, id: Id<TypeNode>) -> csharp::Type {
2037 let mapping = ast
2038 .any_of_type(id)
2039 .ok()
2040 .and_then(|any| ast.facets(any))
2041 .and_then(|facets| facets.corba.as_ref())
2042 .and_then(|corba| corba.native_mapping.clone());
2043 match mapping {
2044 Some(spelling) => {
2045 let segments: Vec<&str> = spelling.split("::").flat_map(|s| s.split('.')).collect();
2046 csharp::Type::named(Path::new(&segments))
2047 }
2048 None => csharp::Type::Predefined(csharp::Predefined::Object),
2049 }
2050}
2051
2052/// A named type's path, rooted at the global namespace.
2053///
2054/// `global::a.b.Point` and not `a.b.Point`. C# resolves a type name
2055/// against the enclosing namespaces first, so inside `namespace
2056/// common.a` the segment `a` binds to `common.a` before the global `a` —
2057/// and only in the corpora that happen to nest that way. `global::`
2058/// resolves at the global namespace and nothing in scope can capture it.
2059///
2060/// # Errors
2061/// [`MaterialiseError::NoModule`] when the model will not answer for a
2062/// module the type nests in.
2063fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
2064 let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
2065 construct: "type".to_string(),
2066 at: "a member".to_string(),
2067 })?;
2068 let mut segments = Vec::new();
2069 for (_, name) in module_chain(ast, node.module)? {
2070 segments.push(name);
2071 }
2072 segments.push(csharp::escape_keyword(&node.name));
2073 Ok(Path::rooted(
2074 &segments.iter().map(String::as_str).collect::<Vec<&str>>(),
2075 ))
2076}
2077
2078/// The map entry class a `map<K,V>` needs a name for.
2079///
2080/// # Errors
2081/// As [`type_tag`].
2082pub fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> Result<String, MaterialiseError> {
2083 let Ok(node) = ast.type_(id) else {
2084 return Err(MaterialiseError::Unevaluated {
2085 construct: "map".to_string(),
2086 at: ENTRY_PREFIX.to_string(),
2087 });
2088 };
2089 let TypeKind::Map { key, value, .. } = &node.kind else {
2090 return Err(MaterialiseError::Unevaluated {
2091 construct: "map".to_string(),
2092 at: named(&node.name, node.node_id),
2093 });
2094 };
2095 let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
2096 return Err(MaterialiseError::Unevaluated {
2097 construct: "map key or value".to_string(),
2098 at: named(&node.name, node.node_id),
2099 });
2100 };
2101 Ok(format!(
2102 "{ENTRY_PREFIX}{}_{}",
2103 type_tag(ast, key, 0)?,
2104 type_tag(ast, value, 0)?
2105 ))
2106}
2107
2108/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
2109/// that keeps two of them apart.
2110///
2111/// The tag also names the routine a hoisted collection's codec is
2112/// emitted under, and one body is emitted per name. With the bound left
2113/// out, `sequence<long>` and `sequence<long, 4>` are one name and
2114/// whichever was declared first decides: measured on
2115/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
2116/// generated C# held no `zd_over` check at all, so `a` accepted a
2117/// stream of any length; with the members the other way round both got
2118/// the bounded body, so `b` refused — and truncated — a length the IDL
2119/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
2120/// beside `wstring` collapse the same way, onto the trimming call.
2121/// Either way the reader answered with a value the IDL does not
2122/// describe and said nothing, which is why the bound is part of the
2123/// name. The same suffix the Zig column carries.
2124///
2125/// An `array` needs nothing here: its `dims` are already in the tag,
2126/// under the `Array` arm below.
2127fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
2128 match bound.expr() {
2129 None => String::new(),
2130 // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
2131 // arm answers for an array of four `long`, and the two are
2132 // different wire forms.
2133 Some(expr) => match ast.eval_integral(expr) {
2134 Ok(limit) => format!("_b{limit}"),
2135 // As the array's `_x`: a bound that does not evaluate is
2136 // still not the unbounded form, and the materialiser
2137 // reports the expression that would not evaluate.
2138 Err(_) => "_bx".to_string(),
2139 },
2140 }
2141}
2142
2143/// The name component an entry class takes from one of its halves.
2144///
2145/// The IDL kind and not the C# spelling. C# distinguishes `octet` from
2146/// `unsigned short` where Java cannot, so this column has fewer
2147/// collisions to avoid than that one — but `char` and `wchar` are both a
2148/// C# `char` and are one and two octets on the wire, and an entry class
2149/// named after the C# type would give `map<char,long>` and
2150/// `map<wchar,long>` one class. A codec written against it would put one
2151/// of the two out at the other's width with nothing to say it had. An
2152/// alias is followed through, because a typedef is transparent here and
2153/// the two names would otherwise describe one class twice.
2154///
2155/// # Errors
2156/// [`MaterialiseError`] when a module or an array dimension will not
2157/// answer.
2158///
2159/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
2160pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
2161 if depth > MAX_TYPE_DEPTH {
2162 return Err(MaterialiseError::Unevaluated {
2163 construct: format!("type, whose name nests past {MAX_TYPE_DEPTH}"),
2164 at: ENTRY_PREFIX.to_string(),
2165 });
2166 }
2167 let Ok(node) = ast.type_(id) else {
2168 return Ok("unknown".to_string());
2169 };
2170 Ok(match &node.kind {
2171 TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
2172 TypeKind::String {
2173 width: CharWidth::Char,
2174 bound,
2175 } => format!("string{}", bound_tag(ast, bound)),
2176 TypeKind::String {
2177 width: CharWidth::WChar,
2178 bound,
2179 } => format!("wstring{}", bound_tag(ast, bound)),
2180 TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
2181 TypeKind::Sequence { element, bound } => match element.resolved() {
2182 Some(element) => format!(
2183 "seq{}_{}",
2184 bound_tag(ast, bound),
2185 type_tag(ast, element, depth + 1)?
2186 ),
2187 None => format!("seq_unknown{}", bound_tag(ast, bound)),
2188 },
2189 TypeKind::Array { element, dims } => {
2190 let mut name = match element.resolved() {
2191 Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)?),
2192 None => "arr_unknown".to_string(),
2193 };
2194 for dim in dims {
2195 match ast.eval_integral(*dim) {
2196 Ok(length) => name.push_str(&format!("_{length}")),
2197 // A dimension that does not evaluate gets no number
2198 // rather than a zero: the name has to differ from a
2199 // `[0]` array's, and `declare` reports the failure by
2200 // name.
2201 Err(_) => name.push_str("_x"),
2202 }
2203 }
2204 name
2205 }
2206 // The entry class's own name carries no bound — two maps that
2207 // differ only in bound hold the same pairs — but the tag does,
2208 // for the routine-name reason in [`bound_tag`].
2209 TypeKind::Map { key, value, bound } => {
2210 let key = match key.resolved() {
2211 Some(key) => type_tag(ast, key, depth + 1)?,
2212 None => "unknown".to_string(),
2213 };
2214 let value = match value.resolved() {
2215 Some(value) => type_tag(ast, value, depth + 1)?,
2216 None => "unknown".to_string(),
2217 };
2218 format!("map{}_{key}_{value}", bound_tag(ast, bound))
2219 }
2220 TypeKind::Alias { aliased } => match aliased.resolved() {
2221 Some(target) => type_tag(ast, target, depth + 1)?,
2222 None => "unknown".to_string(),
2223 },
2224 TypeKind::ValueBox { boxed } => match boxed.resolved() {
2225 Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
2226 None => "box_unknown".to_string(),
2227 },
2228 TypeKind::Structure { .. }
2229 | TypeKind::Union { .. }
2230 | TypeKind::Enumeration { .. }
2231 | TypeKind::Bitmask { .. }
2232 | TypeKind::Bitset { .. }
2233 | TypeKind::Valuetype { .. }
2234 | TypeKind::Native => {
2235 let mut segments = Vec::new();
2236 for (_, name) in module_chain(ast, node.module)? {
2237 segments.push(name);
2238 }
2239 segments.push(csharp::escape_keyword(&node.name));
2240 // A `@` is a lexical escape and not part of a name; leaving
2241 // it in would put one in the middle of a class name, where
2242 // it is not an escape at all.
2243 segments.join("_").replace('@', "")
2244 }
2245 TypeKind::Any => "any".to_string(),
2246 TypeKind::ValueBase => "valuebase".to_string(),
2247 TypeKind::ObjectRef { .. } => "objectref".to_string(),
2248 TypeKind::AnnotationType { .. } => "annotation".to_string(),
2249 })
2250}
2251
2252// -- constants ------------------------------------------------------------
2253
2254/// The C# expression a constant's value takes.
2255///
2256/// The declared type already carries the three string-shaped forms:
2257/// [`csharp_member_type`] answers `string` for a `string`, a `wstring`
2258/// and a `fixed`, so unlike C++ nothing here has to substitute a type.
2259/// What it does have to do is pick the right literal for each and refuse
2260/// the one C# has no literal for.
2261///
2262/// `long double` is refused by name. .NET has no 128-bit floating type,
2263/// the generated `Float128` is a struct with no literal and no `const`
2264/// form at all, and a `double` would put a different number in the file
2265/// than the IDL wrote.
2266///
2267/// # Errors
2268/// [`MaterialiseError`] naming the constant and what will not hold it.
2269fn constant_expr(
2270 type_: &csharp::Type,
2271 value: &zerodds_ast::ConstValue,
2272 at: &str,
2273) -> Result<csharp::Expr, MaterialiseError> {
2274 use zerodds_ast::ConstValue;
2275 let refuse = |construct: String| MaterialiseError::NoLanguageForm {
2276 construct,
2277 language: "C#",
2278 at: at.to_string(),
2279 };
2280 match value {
2281 ConstValue::Integer(number) => literal_of(type_, *number, at),
2282 ConstValue::Boolean(flag) => Ok(csharp::Expr::Literal(Literal::Bool(*flag))),
2283 ConstValue::Char(character) => Ok(csharp::Expr::Literal(Literal::Char(*character))),
2284 // A C# `char` is one UTF-16 code unit, so a code point above the
2285 // Basic Multilingual Plane does not fit one and is refused
2286 // rather than truncated into a lone surrogate.
2287 ConstValue::WChar(point) => match constant::wchar_char(*point)
2288 .filter(|character| u32::from(*character) <= u32::from(u16::MAX))
2289 {
2290 Some(character) => Ok(csharp::Expr::Literal(Literal::Char(character))),
2291 None => Err(refuse(format!(
2292 "the code point U+{point:04X}, which a C# `char` does not hold"
2293 ))),
2294 },
2295 ConstValue::Float { mantissa, exponent } => {
2296 let csharp::Type::Predefined(
2297 predefined @ (csharp::Predefined::Float | csharp::Predefined::Double),
2298 ) = type_
2299 else {
2300 return Err(refuse(String::from(
2301 "a `long double` constant, which has no C# literal",
2302 )));
2303 };
2304 let mut text = constant::float_text(mantissa, exponent.as_deref());
2305 // Without the suffix a real literal is a `double`, and
2306 // `const float f = 1.5;` is a compile error rather than a
2307 // narrowing C# performs for you.
2308 if matches!(predefined, csharp::Predefined::Float) {
2309 text.push('f');
2310 }
2311 Ok(csharp::Expr::Literal(Literal::Float(text)))
2312 }
2313 ConstValue::Fixed { digits, scale } => Ok(csharp::Expr::Literal(Literal::Str(
2314 constant::fixed_decimal(digits, *scale),
2315 ))),
2316 ConstValue::String(text) => Ok(csharp::Expr::Literal(Literal::Str(text.clone()))),
2317 ConstValue::WString(points) => match constant::wstring_utf8(points) {
2318 Some(text) => Ok(csharp::Expr::Literal(Literal::Str(text))),
2319 None => Err(refuse(String::from(
2320 "a `wstring` constant holding a lone surrogate",
2321 ))),
2322 },
2323 // `ConstValue` is `#[non_exhaustive]`, so a form added to the
2324 // model reaches here. Refused by name rather than given a
2325 // plausible default: a constant emitted as the wrong literal is
2326 // a field that disagrees with the IDL and compiles.
2327 _ => Err(refuse(String::from(
2328 "a constant form this binding has no literal for",
2329 ))),
2330 }
2331}
2332
2333/// The integer literal a constant of `type_` takes.
2334///
2335/// A named type here is a generated `enum`, and the value is cast to it:
2336/// an explicit enumeration conversion from a constant is itself a
2337/// constant expression, so it may initialise a `const`.
2338///
2339/// No suffix is written. A C# integer literal takes the first of `int`,
2340/// `uint`, `long` and `ulong` that holds it (ECMA-334 §6.4.5.3), and an
2341/// implicit constant conversion carries an in-range `int` constant to
2342/// every narrower type — so the decimal alone lands in the declared type
2343/// under every one of them. `long.MinValue` is the one special case, and
2344/// §6.4.5.3 spells that one out too.
2345///
2346/// # Errors
2347/// [`MaterialiseError::NoLanguageForm`] when the value does not fit the
2348/// type — never a truncation.
2349fn literal_of(
2350 type_: &csharp::Type,
2351 value: i128,
2352 at: &str,
2353) -> Result<csharp::Expr, MaterialiseError> {
2354 let (predefined, enum_path) = match type_ {
2355 csharp::Type::Predefined(predefined) => (*predefined, None),
2356 // A constant of enumerated type: the value is the enumerator's,
2357 // and `int` is what XTypes 1.3 7.4.3.5.3 makes it.
2358 csharp::Type::Named { path, .. } => (csharp::Predefined::Int, Some(path.clone())),
2359 _ => {
2360 return Err(MaterialiseError::Unevaluated {
2361 construct: "constant's value, which is not an integer".to_string(),
2362 at: at.to_string(),
2363 });
2364 }
2365 };
2366 let literal = match predefined {
2367 csharp::Predefined::Bool => {
2368 return Ok(csharp::Expr::Literal(Literal::Bool(value != 0)));
2369 }
2370 csharp::Predefined::Char => {
2371 let point = in_range(value, 0, i128::from(u16::MAX), "char", at)?;
2372 return Ok(csharp::Expr::Cast {
2373 type_: Box::new(csharp::Type::Predefined(csharp::Predefined::Char)),
2374 value: Box::new(csharp::Expr::Literal(Literal::int(point))),
2375 });
2376 }
2377 csharp::Predefined::SByte => in_range(value, -128, 127, "sbyte", at)?,
2378 csharp::Predefined::Byte => in_range(value, 0, 255, "byte", at)?,
2379 csharp::Predefined::Short => in_range(
2380 value,
2381 i128::from(i16::MIN),
2382 i128::from(i16::MAX),
2383 "short",
2384 at,
2385 )?,
2386 csharp::Predefined::UShort => in_range(value, 0, i128::from(u16::MAX), "ushort", at)?,
2387 csharp::Predefined::Int => {
2388 in_range(value, i128::from(i32::MIN), i128::from(i32::MAX), "int", at)?
2389 }
2390 csharp::Predefined::UInt => in_range(value, 0, i128::from(u32::MAX), "uint", at)?,
2391 csharp::Predefined::Long => in_range(
2392 value,
2393 i128::from(i64::MIN),
2394 i128::from(i64::MAX),
2395 "long",
2396 at,
2397 )?,
2398 csharp::Predefined::ULong => in_range(value, 0, i128::from(u64::MAX), "ulong", at)?,
2399 // An integral value declared as a real: the literal is the
2400 // number, and C# widens an int constant to it.
2401 csharp::Predefined::Float | csharp::Predefined::Double => value,
2402 _ => {
2403 return Err(MaterialiseError::Unevaluated {
2404 construct: "constant's value, which is not an integer".to_string(),
2405 at: at.to_string(),
2406 });
2407 }
2408 };
2409 let expr = csharp::Expr::Literal(Literal::int(literal));
2410 match enum_path {
2411 None => Ok(expr),
2412 Some(path) => Ok(csharp::Expr::Cast {
2413 type_: Box::new(csharp::Type::named(path)),
2414 value: Box::new(expr),
2415 }),
2416 }
2417}
2418
2419/// `value`, or the failure that says it does not fit.
2420///
2421/// # Errors
2422/// [`MaterialiseError::NoLanguageForm`], naming the value and the type
2423/// that will not hold it. Truncating instead would put a number nobody
2424/// wrote into the output.
2425fn in_range(
2426 value: i128,
2427 low: i128,
2428 high: i128,
2429 type_: &str,
2430 at: &str,
2431) -> Result<i128, MaterialiseError> {
2432 if value < low || value > high {
2433 return Err(MaterialiseError::NoLanguageForm {
2434 construct: format!("the value {value}, which a C# `{type_}` does not hold"),
2435 language: "C#",
2436 at: at.to_string(),
2437 });
2438 }
2439 Ok(value)
2440}
2441
2442// -- shared -------------------------------------------------------------
2443
2444/// A model node's name, or its identity when the node is anonymous.
2445///
2446/// An array type declared inline has no name of its own, and an error
2447/// that said only "the array length does not evaluate" would leave the
2448/// reader to find which array.
2449fn named(name: &str, node: NodeId) -> String {
2450 if name.is_empty() {
2451 format!("{node:?}")
2452 } else {
2453 name.to_string()
2454 }
2455}
2456
2457/// Whether the rules created a node of `type_name` for `node`.
2458///
2459/// The same two-hop walk the other materialisers do: a correspondence is
2460/// an ordinary node with a link to the anchor and a link to what was
2461/// created.
2462fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
2463 let anchor = applied
2464 .origin
2465 .iter()
2466 .find(|(_, model)| **model == node)
2467 .map(|(ghost, _)| *ghost)?;
2468 let typ = applied.graph.types.lookup(type_name)?;
2469 applied
2470 .graph
2471 .nodes_of_type(typ)
2472 .map(|created| created.id)
2473 .find(|created| {
2474 applied.graph.connected(&anchor, created)
2475 || applied
2476 .graph
2477 .parts(&anchor)
2478 .any(|part| applied.graph.connected(&part.other, created))
2479 })
2480}
2481
2482#[cfg(test)]
2483mod tests {
2484 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2485 use super::*;
2486 use crate::rule;
2487 use crate::{ModelGraph, apply};
2488 use std::path::{Path as FsPath, PathBuf};
2489
2490 /// IDL in, one C# file out.
2491 fn render(idl: &str, path: &str) -> Result<String, String> {
2492 let parsed = zerodds_idl_parse::parse(idl);
2493 if parsed.has_errors() {
2494 return Err(format!("parse: {:?}", parsed.diagnostics()));
2495 }
2496 let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
2497 if !lowered.diagnostics.is_empty() {
2498 return Err(format!("lower: {:?}", lowered.diagnostics));
2499 }
2500 let mut ast = lowered.ast;
2501 ast.resolve_all_by_name()
2502 .map_err(|error| format!("resolve: {error}"))?;
2503 let applied = apply(&ModelGraph::of(&ast), &rule::csharp::catalogue())
2504 .map_err(|error| format!("apply: {error}"))?;
2505 let mut file =
2506 csharp_file(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
2507 file.doc = Doc::lines(&[
2508 "Generated by zerodds-lang-rules. Do not edit.",
2509 "",
2510 "The C# shape of one IDL file. The wire rules are not here - they",
2511 "come from DDS-XTypes 1.3 and belong to the codec.",
2512 ]);
2513 Ok(zerodds_lang_render::csharp::file(&file))
2514 }
2515
2516 /// The same, for a source with no file behind it.
2517 fn declarations(idl: &str) -> String {
2518 match render(idl, "test.idl") {
2519 Ok(text) => text,
2520 Err(why) => panic!("{why}"),
2521 }
2522 }
2523
2524 /// Every corpus case, by name.
2525 fn corpus() -> Vec<(String, PathBuf)> {
2526 let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
2527 let mut out = Vec::new();
2528 for entry in std::fs::read_dir(&root).expect("the corpus directory") {
2529 let entry = entry.expect("a corpus entry");
2530 let idl = entry.path().join("type.idl");
2531 if idl.is_file() {
2532 out.push((entry.file_name().to_string_lossy().into_owned(), idl));
2533 }
2534 }
2535 out.sort();
2536 out
2537 }
2538
2539 /// The whole corpus, through every layer, with nothing skipped.
2540 ///
2541 /// A rendered file is not a compiled one. `ZD_CSHARP_OUT` names a
2542 /// directory to write the sources into, and a C# compiler over that
2543 /// directory is the second half of the check — a file that does not
2544 /// compile is a materialiser defect, not a corpus defect.
2545 #[test]
2546 fn every_corpus_case_renders_or_names_its_failure() {
2547 let cases = corpus();
2548 assert!(
2549 cases.len() > 300,
2550 "the corpus is not where it was: {} cases",
2551 cases.len()
2552 );
2553 let out = std::env::var("ZD_CSHARP_OUT").ok();
2554 let mut failures = Vec::new();
2555 for (name, path) in cases {
2556 let source = std::fs::read_to_string(&path).expect("a corpus source");
2557 match render(&source, &path.display().to_string()) {
2558 Ok(text) => {
2559 if let Some(dir) = &out {
2560 let case = FsPath::new(dir).join(&name);
2561 std::fs::create_dir_all(&case).expect("the output directory");
2562 std::fs::write(case.join("ZdTypes.cs"), text).expect("the output file");
2563 }
2564 }
2565 Err(why) => failures.push(format!("{name}: {why}")),
2566 }
2567 }
2568
2569 // The five constructs no corpus case declares. The corpus is a
2570 // wire corpus, so it carries what goes on the wire; `const`,
2571 // `exception`, `valuetype`, `native` and `interface` are in the
2572 // catalogue all the same, and a column whose compile gate ran
2573 // only over the corpus would be claiming they compile rather
2574 // than showing it. Dumped beside the cases, so whatever compiler
2575 // walks that directory walks these too.
2576 match render(UNREACHABLE_BY_THE_CORPUS, "unreachable-constructs.idl") {
2577 Ok(text) => {
2578 if let Some(dir) = &out {
2579 let case = FsPath::new(dir).join("_unreachable-constructs");
2580 std::fs::create_dir_all(&case).expect("the output directory");
2581 std::fs::write(case.join("ZdTypes.cs"), text).expect("the output file");
2582 }
2583 }
2584 Err(why) => failures.push(format!("_unreachable-constructs: {why}")),
2585 }
2586
2587 let failures = crate::adapter::without_the_front_end_refusals(failures);
2588 assert!(failures.is_empty(), "{failures:#?}");
2589 }
2590
2591 /// One source carrying every construct the wire corpus does not.
2592 const UNREACHABLE_BY_THE_CORPUS: &str = "
2593 module reach {
2594 const long MAX = 10;
2595 const string NAME = \"zerodds\";
2596 const double RATIO = 1.5;
2597 typedef sequence<long> Row;
2598 native Handle;
2599 exception Overrun { long limit; string message; };
2600 valuetype Node { public long id; public string label; };
2601 interface Robot {
2602 long move(in long distance, out double travelled, inout long budget);
2603 void halt();
2604 };
2605 @final struct Sample { Row r; Handle h; Node n; long tail; };
2606 };
2607 ";
2608
2609 #[test]
2610 fn every_idl_primitive_reaches_the_csharp_type_that_holds_it() {
2611 let generated = declarations(
2612 "@final struct P {
2613 boolean a; octet b; char c; wchar d;
2614 int8 e; uint8 f; short g; unsigned short h;
2615 long i; unsigned long j; long long k; unsigned long long l;
2616 float m; double n; long double o;
2617 };",
2618 );
2619 for expected in [
2620 "public bool A;",
2621 "public byte B;",
2622 "public char C;",
2623 "public char D;",
2624 "public sbyte E;",
2625 "public byte F;",
2626 "public short G;",
2627 "public ushort H;",
2628 // IDL `long` is 32 bits; a C# `long` is 64.
2629 "public int I;",
2630 "public uint J;",
2631 "public long K;",
2632 "public ulong L;",
2633 "public float M;",
2634 "public double N;",
2635 "public global::ZeroDds.Idl.Float128 O;",
2636 ] {
2637 assert!(generated.contains(expected), "{expected}\n{generated}");
2638 }
2639 }
2640
2641 #[test]
2642 fn every_field_carries_the_idl_name_it_was_folded_from() {
2643 // The fold is what makes the attribute load-bearing: a member's
2644 // ID under @autoid(HASH) is the MD5 of `venue`, not of `Venue`.
2645 let generated = declarations("@final struct P { long venue; long my_field; };");
2646 assert!(
2647 generated.contains("[global::ZeroDds.Idl.IdlName(\"venue\")]"),
2648 "{generated}"
2649 );
2650 assert!(generated.contains("public int Venue;"), "{generated}");
2651 // Only the first letter moves: a full PascalCase fold is not
2652 // injective over the names IDL allows in one scope.
2653 assert!(generated.contains("public int My_field;"), "{generated}");
2654 }
2655
2656 #[test]
2657 fn two_members_that_differ_only_after_the_fold_stay_two_members() {
2658 // `my_field` and `myField` are both legal in one IDL struct and
2659 // both fold to `MyField` under a full PascalCase. Under this
2660 // one they do not collide at all.
2661 let generated = declarations("@final struct P { long my_field; long myField; };");
2662 assert!(generated.contains("public int My_field;"), "{generated}");
2663 assert!(generated.contains("public int MyField;"), "{generated}");
2664 }
2665
2666 #[test]
2667 fn a_member_that_would_collide_with_its_type_cannot_come_from_idl() {
2668 // CS0542 forbids a member sharing the simple name of its type,
2669 // and `fields_of` moves such a member to `Name_` with an
2670 // `[IdlName]` attribute keeping the IDL spelling.
2671 //
2672 // That move is no longer reachable **from IDL**, and the test
2673 // says so rather than asserting a shape nothing can produce.
2674 // `capitalise` lifts the first letter and nothing else, so a
2675 // member folds onto its type's name only when the two differ in
2676 // that letter alone — which IDL 4.2 §7.2.3 refuses, and the model
2677 // refuses it before any backend runs. The same argument covers a
2678 // collision between two members.
2679 //
2680 // The logic stays because a model does not have to come from
2681 // IDL: `dynamic::to_ast` builds one from a peer's TypeObjects,
2682 // where §7.2.3 was that peer's rule to keep and not ours to
2683 // assume.
2684 let refused = render("@final struct Point { long point; };", "test.idl");
2685 assert!(
2686 refused.is_err(),
2687 "§7.2.3 stops this before C# sees it: {refused:?}"
2688 );
2689
2690 // The neighbouring shape that IDL does admit, to show where the
2691 // line runs: an underscore is not a case difference, and both
2692 // names survive with distinct C# spellings.
2693 let generated = declarations("@final struct MyField { long my_field; };");
2694 assert!(
2695 generated.contains("public sealed class MyField"),
2696 "{generated}"
2697 );
2698 assert!(generated.contains("public int My_field;"), "{generated}");
2699 }
2700
2701 #[test]
2702 fn a_sequence_and_an_array_are_both_csharp_arrays() {
2703 let generated = declarations(
2704 "@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
2705 );
2706 assert!(generated.contains("public int[] S;"), "{generated}");
2707 assert!(generated.contains("public int[] A;"), "{generated}");
2708 assert!(generated.contains("public int[][] G;"), "{generated}");
2709 assert!(generated.contains("public string[] T;"), "{generated}");
2710 }
2711
2712 #[test]
2713 fn a_map_is_an_array_of_a_generated_entry_class() {
2714 let generated = declarations("@final struct P { map<long, string> m; };");
2715 // The tag is the IDL kind's spelling: `char` and `wchar` are
2716 // both a C# `char` and are one and two octets on the wire.
2717 assert!(
2718 generated.contains("class ZdEntry_long_string"),
2719 "{generated}"
2720 );
2721 assert!(generated.contains("public int Key;"), "{generated}");
2722 assert!(generated.contains("public string Value;"), "{generated}");
2723 assert!(
2724 generated.contains("public global::ZdEntry_long_string[] M;"),
2725 "{generated}"
2726 );
2727 // No field is a Dictionary; the entry order is the sample's.
2728 // (The entry class's own prose names the type it is not, so the
2729 // check is on a field and not on the text.)
2730 assert!(
2731 !generated.contains("public global::System.Collections"),
2732 "{generated}"
2733 );
2734 }
2735
2736 #[test]
2737 fn two_maps_of_one_shape_are_one_entry_class() {
2738 let generated = declarations("@final struct P { map<long, long> a; map<long, long> b; };");
2739 assert_eq!(generated.matches("class ZdEntry_long_long").count(), 1);
2740 }
2741
2742 #[test]
2743 fn two_maps_whose_csharp_spelling_coincides_are_still_two_entry_classes() {
2744 // `char` and `wchar` are both a C# `char` and are one and two
2745 // octets on the wire. One entry class for both would put one of
2746 // them out at the other's width.
2747 let generated = declarations("@final struct P { map<char, long> a; map<wchar, long> b; };");
2748 assert!(generated.contains("class ZdEntry_char_long"), "{generated}");
2749 assert!(
2750 generated.contains("class ZdEntry_wchar_long"),
2751 "{generated}"
2752 );
2753 }
2754
2755 #[test]
2756 fn an_optional_value_type_is_nullable_and_a_reference_one_is_not() {
2757 let generated = declarations(
2758 "@mutable struct P {
2759 @optional long a; @optional string b; @optional long long c;
2760 };",
2761 );
2762 // An IDL `long` is 32 bits, so the nullable is `int?`.
2763 assert!(generated.contains("public int? A;"), "{generated}");
2764 // A `string` is a reference and nullable already; `string?`
2765 // outside a #nullable context is CS8632.
2766 assert!(generated.contains("public string B;"), "{generated}");
2767 assert!(!generated.contains("string?"), "{generated}");
2768 assert!(generated.contains("public long? C;"), "{generated}");
2769 }
2770
2771 #[test]
2772 fn an_optional_enum_is_nullable_because_an_enum_is_a_value_type() {
2773 let generated = declarations(
2774 "enum Colour { RED, GREEN };
2775 @mutable struct P { @optional Colour c; };",
2776 );
2777 assert!(
2778 generated.contains("public global::Colour? C;"),
2779 "{generated}"
2780 );
2781 }
2782
2783 #[test]
2784 fn a_union_is_a_class_with_a_discriminator_and_one_field_per_case() {
2785 let generated = declarations(
2786 "union Choice switch (long) {
2787 case 1: long whole;
2788 case 2: long other;
2789 default: double fraction;
2790 };
2791 @final struct P { Choice c; };",
2792 );
2793 assert!(generated.contains("public int _d;"), "{generated}");
2794 assert!(generated.contains("public int Whole;"), "{generated}");
2795 assert!(generated.contains("public int Other;"), "{generated}");
2796 assert!(generated.contains("public double Fraction;"), "{generated}");
2797 assert!(
2798 generated.contains("public global::Choice C;"),
2799 "{generated}"
2800 );
2801 }
2802
2803 #[test]
2804 fn an_enumeration_is_a_real_enum_and_a_member_of_it_is_that_type() {
2805 let generated = declarations(
2806 "enum Colour { RED, GREEN, BLUE };
2807 @final struct P { Colour c; };",
2808 );
2809 assert!(
2810 generated.contains("public enum Colour : int"),
2811 "{generated}"
2812 );
2813 assert!(generated.contains("RED = 0,"), "{generated}");
2814 assert!(generated.contains("BLUE = 2,"), "{generated}");
2815 // The type and not an `int`: a C# enum member's identity is the
2816 // integer it is given, which is the Java column's objection.
2817 assert!(
2818 generated.contains("public global::Colour C;"),
2819 "{generated}"
2820 );
2821 }
2822
2823 #[test]
2824 fn an_explicit_enumerator_value_is_written_out() {
2825 let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
2826 assert!(generated.contains("LOW = -1,"), "{generated}");
2827 assert!(generated.contains("HIGH = 70000,"), "{generated}");
2828 }
2829
2830 #[test]
2831 fn a_bitmask_is_a_flags_enum_over_the_holder_its_bound_calls_for() {
2832 let narrow =
2833 declarations("@bit_bound(8) bitmask Small { @position(0) A, @position(7) B };");
2834 assert!(narrow.contains("public enum Small : byte"), "{narrow}");
2835 assert!(narrow.contains("[global::System.Flags]"), "{narrow}");
2836 assert!(narrow.contains("B = 128,"), "{narrow}");
2837 let wide = declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
2838 assert!(wide.contains("public enum Big : ulong"), "{wide}");
2839 // The value and not `1 << 40`: a C# shift is typed by its left
2840 // operand, and `1 << 40` is an int shift evaluating to 256.
2841 assert!(wide.contains("B = 1099511627776,"), "{wide}");
2842 }
2843
2844 #[test]
2845 fn a_bitset_is_a_class_of_one_field_per_bitfield() {
2846 let generated = declarations(
2847 "bitset Packed { bitfield<3> low; bitfield<5> high; };
2848 @final struct P { Packed value; };",
2849 );
2850 assert!(generated.contains("public byte Low;"), "{generated}");
2851 assert!(generated.contains("public byte High;"), "{generated}");
2852 assert!(
2853 generated.contains("public global::Packed Value;"),
2854 "{generated}"
2855 );
2856 }
2857
2858 #[test]
2859 fn a_fixed_is_a_string_and_not_a_decimal() {
2860 let generated = declarations(
2861 "typedef fixed<9,3> Money;
2862 @final struct P { Money amount; };",
2863 );
2864 assert!(generated.contains("public string Amount;"), "{generated}");
2865 // No field takes one. (`decimal` appears in the support
2866 // namespace's prose, which names the type it is not.)
2867 assert!(!generated.contains("public decimal"), "{generated}");
2868 }
2869
2870 #[test]
2871 fn a_typedef_is_a_using_alias_and_every_use_takes_the_resolved_type() {
2872 let generated = declarations(
2873 "typedef sequence<long> Row;
2874 @final struct P { Row r; };",
2875 );
2876 assert!(generated.contains("using Row = int[];"), "{generated}");
2877 // The use takes the resolved type: a `using` alias may not be
2878 // built on another one in the same scope.
2879 assert!(generated.contains("public int[] R;"), "{generated}");
2880 }
2881
2882 #[test]
2883 fn a_module_is_a_namespace_and_two_modules_do_not_collide() {
2884 let generated = declarations(
2885 "module a { @final struct P { long x; }; };
2886 module b { @final struct P { double y; }; };
2887 @final struct Q { a::P one; b::P two; };",
2888 );
2889 assert!(generated.contains("namespace a"), "{generated}");
2890 assert!(generated.contains("namespace b"), "{generated}");
2891 assert!(generated.contains("public global::a.P One;"), "{generated}");
2892 assert!(generated.contains("public global::b.P Two;"), "{generated}");
2893 }
2894
2895 #[test]
2896 fn a_module_reopened_beside_a_nested_one_is_one_namespace_block() {
2897 let generated = declarations(
2898 "module common {
2899 module util { @final struct Location { double x; }; };
2900 @final struct Robot { ::common::util::Location where_; };
2901 };",
2902 );
2903 assert_eq!(
2904 generated.matches("namespace common\n").count(),
2905 1,
2906 "{generated}"
2907 );
2908 assert!(
2909 generated.contains("public global::common.util.Location Where_;"),
2910 "{generated}"
2911 );
2912 }
2913
2914 #[test]
2915 fn an_identifier_that_is_a_csharp_keyword_is_escaped_with_an_at_sign() {
2916 let generated = declarations(
2917 "module namespace {
2918 enum class { template, virtual, friend };
2919 @final struct type { long new; };
2920 };",
2921 );
2922 assert!(generated.contains("namespace @namespace"), "{generated}");
2923 assert!(generated.contains("enum @class"), "{generated}");
2924 assert!(generated.contains("class type"), "{generated}");
2925 // A member is folded before it is escaped, and `New` is not a
2926 // keyword — so the escape never fires on a member that the fold
2927 // has already moved out of the keyword space. The IDL spelling
2928 // is on the attribute either way.
2929 assert!(generated.contains("public int New;"), "{generated}");
2930 assert!(
2931 generated.contains("[global::ZeroDds.Idl.IdlName(\"new\")]"),
2932 "{generated}"
2933 );
2934 // `virtual` is a C# keyword and moves; `template` and `friend`
2935 // are C++ keywords and are identifiers here. Enumerator names
2936 // are not folded, so the escape is what they meet.
2937 assert!(generated.contains("@virtual = 1,"), "{generated}");
2938 assert!(generated.contains("template = 0,"), "{generated}");
2939 }
2940
2941 #[test]
2942 fn a_constant_becomes_a_const_field_of_a_holder_class() {
2943 let generated = declarations(
2944 "const long MAX = 10;
2945 const string NAME = \"zerodds\";
2946 const unsigned long WIDE = 4294967295;",
2947 );
2948 assert!(generated.contains("class ZdConstants"), "{generated}");
2949 assert!(
2950 generated.contains("public const int MAX = 10;"),
2951 "{generated}"
2952 );
2953 assert!(
2954 generated.contains("public const string NAME = \"zerodds\";"),
2955 "{generated}"
2956 );
2957 // A C# integer literal takes the first of int, uint, long, ulong
2958 // that holds it, so no suffix is needed.
2959 assert!(
2960 generated.contains("public const uint WIDE = 4294967295;"),
2961 "{generated}"
2962 );
2963 }
2964
2965 #[test]
2966 fn a_construct_with_no_csharp_data_form_is_named_and_not_left_out() {
2967 // The member is not dropped and no `object` is substituted for
2968 // it: a class missing a field that reports success is the defect
2969 // this materialiser is arranged against.
2970 for (idl, construct) in [
2971 ("@final struct P { any a; long tail; };", "any"),
2972 ("@final struct P { Object o; long tail; };", "Object"),
2973 ] {
2974 let failure = render(idl, "test.idl").expect_err(construct);
2975 assert!(failure.contains(construct), "{failure}");
2976 assert!(failure.contains("C#"), "{failure}");
2977 }
2978 }
2979
2980 #[test]
2981 fn a_name_that_would_capture_the_rooted_references_is_refused() {
2982 for module in ["System", "ZeroDds"] {
2983 let idl = format!("module {module} {{ @final struct P {{ long x; }}; }};");
2984 let failure = render(&idl, "test.idl").expect_err(module);
2985 assert!(failure.contains(module), "{failure}");
2986 }
2987 }
2988
2989 #[test]
2990 fn an_unevaluated_array_length_is_named_and_not_substituted() {
2991 // The C and Java columns fail on the same input; the three
2992 // columns' failure sets stay comparable.
2993 let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
2994 .expect_err("an array whose length does not evaluate is refused");
2995 assert!(!failure.is_empty(), "{failure}");
2996 }
2997
2998 #[test]
2999 fn an_interface_is_an_interface_of_signatures_with_the_idl_passing_modes() {
3000 // No corpus case declares one, so this is what says the
3001 // `interface-to-interface` rule materialises rather than
3002 // claiming a coverage cell that produces nothing.
3003 let generated = declarations(
3004 "interface Robot {
3005 long move(in long distance, out double travelled, inout long budget);
3006 void halt();
3007 };",
3008 );
3009 assert!(generated.contains("public interface Robot"), "{generated}");
3010 assert!(
3011 generated.contains("int Move(int distance, out double travelled, ref int budget);"),
3012 "{generated}"
3013 );
3014 assert!(generated.contains("void Halt();"), "{generated}");
3015 assert!(
3016 generated.contains("[global::ZeroDds.Idl.IdlName(\"move\")]"),
3017 "{generated}"
3018 );
3019 }
3020
3021 #[test]
3022 fn an_exception_is_a_data_class_and_not_a_system_exception() {
3023 // No corpus case declares one either. An IDL member named
3024 // `message` folds onto `System.Exception.Message`, and hiding an
3025 // inherited member (CS0108) is not a decision the wire makes.
3026 let generated = declarations("exception Overrun { long limit; string message; };");
3027 assert!(
3028 generated.contains("public sealed class Overrun"),
3029 "{generated}"
3030 );
3031 assert!(
3032 !generated.contains(": global::System.Exception"),
3033 "{generated}"
3034 );
3035 assert!(generated.contains("public int Limit;"), "{generated}");
3036 assert!(generated.contains("public string Message;"), "{generated}");
3037 }
3038
3039 #[test]
3040 fn a_valuetype_is_a_class_of_its_state_members() {
3041 let generated = declarations(
3042 "valuetype Node { public long id; public string label; };
3043 @final struct P { Node n; };",
3044 );
3045 assert!(
3046 generated.contains("public sealed class Node"),
3047 "{generated}"
3048 );
3049 assert!(generated.contains("public int Id;"), "{generated}");
3050 assert!(generated.contains("public string Label;"), "{generated}");
3051 assert!(generated.contains("public global::Node N;"), "{generated}");
3052 }
3053
3054 #[test]
3055 fn a_native_type_takes_an_alias_and_object_where_no_facet_names_one() {
3056 let generated = declarations(
3057 "native Handle;
3058 @final struct P { Handle h; };",
3059 );
3060 assert!(generated.contains("using Handle = object;"), "{generated}");
3061 // The use takes the resolved type, as a typedef's does.
3062 assert!(generated.contains("public object H;"), "{generated}");
3063 }
3064
3065 #[test]
3066 fn every_generated_file_carries_the_support_namespace() {
3067 let generated = declarations("@final struct P { long x; };");
3068 assert!(generated.contains("namespace ZeroDds.Idl"), "{generated}");
3069 assert!(
3070 generated.contains("class IdlNameAttribute : global::System.Attribute"),
3071 "{generated}"
3072 );
3073 assert!(
3074 generated.contains("public readonly struct Float128"),
3075 "{generated}"
3076 );
3077 }
3078}