Skip to main content

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_, &param.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(&param.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}