Skip to main content

zerodds_lang_rules/emit/
csharp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The C# backend of the shared codec walk.
4//!
5//! The same walk as the C, C++, Java and TypeScript columns, spelled in
6//! C#. `runtime/csharp` implements DDS-XTypes 1.3 §7.4 from the clause
7//! and calls nothing in Rust, so where two columns disagree on an octet
8//! one of them has misread the specification — which is the only reason
9//! to have a sixth.
10//!
11//! # The four decisions this column turns on
12//!
13//! **The unsigned types are real.** This is the whole difference from
14//! the Java backend, whose [`CodecTarget::scalar`] has to answer `int`
15//! for a `uint8` and `long` for a `uint32` because Java has no unsigned
16//! integer. Here every IDL width maps to a C# keyword of the same width
17//! and the same signedness, and `ulong` holds `18446744073709551615` as
18//! a number rather than as a pattern a caller has to decode.
19//!
20//! **An enumeration is a real `enum`.** `adapter/csharp.rs` declares
21//! `enum Colour : int`, so an enumerator constant is
22//! `global::a.Colour.RED` and not a bare identifier — and a C# `enum`
23//! has no implicit conversion to an integer, which is why the walk's
24//! `(long)` and `(ulong)` casts are kept here where the Java backend
25//! drops them.
26//!
27//! **A member's C# name is folded and its enumerators are not.**
28//! `adapter/csharp.rs` writes `venue` as `Venue` and carries the IDL
29//! spelling in `[IdlName]`, because §7.3.1.2.1.1 computes a hashed
30//! member ID from the IDL name. A type's name and an enumerator's are
31//! verbatim, because a type's name reaches the wire.
32//! [`CodecTarget::escape`] is therefore the *member* fold alone, and
33//! the enumerator seam is [`CodecTarget::enumerator_constant`].
34//!
35//! **`@optional` is `null`, and a value type's `T?` is a different
36//! type from `T`.** That is why [`CodecTarget::optional_value`] takes
37//! the member's type: `int?` does not convert to `int` on its own, and
38//! a `string?` has no `.Value` to take. No other column needed the
39//! parameter and this one cannot be written without it.
40//!
41//! # `long double` and `fixed`, and what they are not
42//!
43//! A `long double` is `global::ZeroDds.Idl.Float128`, a sixteen-octet
44//! carrier. `decimal` is not a binary128 and is not close to one: it is
45//! base ten, has 28 to 29 significant digits against binary128's 34, has
46//! no exponent range past ±28 against ±4932, and has no signed zero, no
47//! infinity and no NaN.
48//!
49//! A `fixed<P,S>` is a `string` holding the decimal, for the reason
50//! `adapter/csharp.rs` gives and the corpus measures: `fixed-scales`
51//! declares `fixed<31,10>` and its value
52//! `-12345678901234567890.1234567890` has thirty significant digits,
53//! which a `decimal` rounds. The `FixedDigitCount::ValuePrecision` axis
54//! exists to measure how many digits a peer writes into the BCD run,
55//! and a holder that normalises its leading zeros away has already
56//! decided that answer.
57
58use super::{BitPatternSource, CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
59use crate::adapter::csharp::{
60    csharp_member_type, declared_path, field_name, map_entry_name, type_tag, unsigned_holder,
61};
62use crate::encode::EncodeError;
63use zerodds_ast::{Ast, MemberNode, TypeNode};
64use zerodds_ast_core::Id;
65use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
66use zerodds_lang_cst::csharp;
67
68/// The C# backend.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct CSharp;
71
72impl CSharp {
73    /// `write_u32` as `WriteU32`.
74    ///
75    /// The runtime's methods are named by .NET's convention and the
76    /// walk asks in the wire's, so one of the two has to convert. Doing
77    /// it here means a new operation is added to the walk under one
78    /// name. `PascalCase` and not the Java backend's `camelCase`,
79    /// because that is what .NET names a public method.
80    fn pascal(method: &str) -> String {
81        let mut out = String::new();
82        let mut rising = true;
83        for character in method.chars() {
84            if character == '_' {
85                rising = true;
86                continue;
87            }
88            if rising {
89                out.extend(character.to_uppercase());
90                rising = false;
91            } else {
92                out.push(character);
93            }
94        }
95        out
96    }
97
98    /// A dotted name as a path, keeping a `global::` root where one is
99    /// written.
100    fn path(name: &str) -> Path {
101        match name.strip_prefix("global::") {
102            Some(rest) => Path::rooted(&rest.split('.').collect::<Vec<_>>()),
103            None => Path::new(&name.split('.').collect::<Vec<_>>()),
104        }
105    }
106
107    /// A reader's body, with the sample it answers with around it.
108    ///
109    /// `<T> v = zd_def_<T>(); … return v;`, exactly as the Java
110    /// backend does and for the same two reasons: the sample starts at
111    /// Table 9's value so a position the stream does not carry is
112    /// already what DDS-XTypes 1.3 says it is, and the defaulter's name
113    /// is the decoder's with the verb changed so the two cannot name
114    /// different types.
115    fn reading_body(name: &str, sample: csharp::Type, body: csharp::Block) -> csharp::Block {
116        let defaulter = crate::default_csharp::defaulter_from_decoder(name);
117        let mut statements = vec![csharp::Statement::Declare {
118            type_: sample,
119            name: Ident::new("v"),
120            value: Some(Self::call(&defaulter, Vec::new())),
121        }];
122        statements.extend(body.statements);
123        statements.push(csharp::Statement::Return(Some(Self::whole("v"))));
124        csharp::Block { statements }
125    }
126
127    /// A type split into its innermost element and how many `[]` it
128    /// carries.
129    ///
130    /// `new int[3][]` is how C# allocates an `int[][]`, and
131    /// `new int[][3]` is not an expression. So an allocation needs the
132    /// element and the rank apart, which is what
133    /// [`csharp::Expr::NewArray::nested`] takes.
134    pub(crate) fn peel(type_: csharp::Type) -> (csharp::Type, usize) {
135        let mut nested = 0;
136        let mut current = type_;
137        // Bounded by the materialiser's own `MAX_TYPE_DEPTH`; a chain
138        // longer than this did not come from a model it accepted.
139        for _ in 0..64 {
140            match current {
141                csharp::Type::Array { element, rank } => {
142                    nested += rank;
143                    current = *element;
144                }
145                other => return (other, nested),
146            }
147        }
148        (current, nested)
149    }
150
151    /// Whether a type is one C# converts to and from with a cast.
152    ///
153    /// Every keyword type but `string` and `object`. A generated
154    /// `Float128` is not one, and a cast to it would not compile.
155    fn is_castable(type_: &csharp::Type) -> bool {
156        match type_ {
157            csharp::Type::Predefined(kind) => !matches!(
158                kind,
159                csharp::Predefined::String
160                    | csharp::Predefined::Object
161                    | csharp::Predefined::Void
162                    | csharp::Predefined::Dynamic
163            ),
164            _ => false,
165        }
166    }
167}
168
169impl CodecTarget for CSharp {
170    type Expr = csharp::Expr;
171    type Stmt = csharp::Statement;
172    type Block = csharp::Block;
173    type Type = csharp::Type;
174    type Decl = csharp::Member;
175
176    // -- types --------------------------------------------------------
177
178    /// One IDL width, one C# keyword, same signedness.
179    ///
180    /// The two counters are the exception and neither is a wire width.
181    /// [`Scalar::Index`] is `int`, which is what a C# array's `Length`
182    /// is and what `new T[n]` and a subscript take. [`Scalar::Size`] is
183    /// `long`, and that is a correctness decision rather than a
184    /// spelling: a sequence's declared count is a `uint`, the reader
185    /// checks it against `ReaderRemaining()` before allocating, and an
186    /// `int` there would make `0xFFFFFFFF` compare as `-1` and pass the
187    /// check it exists to fail.
188    fn scalar(kind: Scalar) -> Self::Type {
189        csharp::Type::Predefined(match kind {
190            Scalar::Bool => csharp::Predefined::Bool,
191            Scalar::Char => csharp::Predefined::Char,
192            Scalar::Int8 => csharp::Predefined::SByte,
193            Scalar::UInt8 => csharp::Predefined::Byte,
194            Scalar::Int16 => csharp::Predefined::Short,
195            Scalar::UInt16 => csharp::Predefined::UShort,
196            Scalar::Index | Scalar::Int32 => csharp::Predefined::Int,
197            Scalar::UInt32 => csharp::Predefined::UInt,
198            Scalar::Size | Scalar::Int64 => csharp::Predefined::Long,
199            Scalar::UInt64 => csharp::Predefined::ULong,
200            Scalar::Float => csharp::Predefined::Float,
201            Scalar::Double => csharp::Predefined::Double,
202            // binary128 has no C# primitive and `decimal` is not one:
203            // base ten, 28 digits, no infinity and no signed zero.
204            Scalar::LongDouble => {
205                return csharp::Type::named(Path::rooted(&["ZeroDds", "Idl", "Float128"]));
206            }
207        })
208    }
209
210    fn named(name: &str) -> Self::Type {
211        csharp::Type::named(Self::path(name))
212    }
213
214    fn nothing() -> Self::Type {
215        csharp::Type::Predefined(csharp::Predefined::Void)
216    }
217
218    /// `runtime/csharp` publishes each handle as a type of its own in
219    /// `ZeroDds.Wire`, and every reference to one is **rooted**.
220    ///
221    /// Not decoration. C# resolves a simple name against the global
222    /// namespace, where every generated type sits, so a corpus case
223    /// declaring `enum Status` puts its own type in front of the
224    /// runtime's `Status` and `Status.Discarded` becomes CS0117 —
225    /// measured on `enum-sparse-values` and its unannotated twin, which
226    /// declare exactly that. `global::` resolves at the global namespace
227    /// and nothing in any scope can capture it, which is the same reason
228    /// `adapter/csharp.rs` roots every path it writes.
229    fn handle(kind: Handle) -> Self::Type {
230        csharp::Type::named(Path::rooted(&[
231            "ZeroDds",
232            "Wire",
233            match kind {
234                Handle::DHeader => "DHeader",
235                Handle::EmHeader => "EmHeader",
236                Handle::Pid => "Pid",
237                Handle::TryConstruct => "TryConstruct",
238                Handle::ReaderMark => "ReaderMark",
239                Handle::PidRead => "PidRead",
240                Handle::EmHeaderRead => "EmHeaderRead",
241            },
242        ]))
243    }
244
245    /// A C# `char` is one UTF-16 code unit, which is what the runtime's
246    /// `WriteChar` and `WriteWchar` take. There is nothing to widen and
247    /// nothing to narrow, so the value goes through: the narrowing an
248    /// IDL `char` needs is the runtime's, which refuses a code unit
249    /// above U+00FF rather than keeping its low byte.
250    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
251        value
252    }
253
254    /// Statics on the runtime's writer, rooted like every other runtime
255    /// reference — [`CodecTarget::runtime_static`] names the *reader*
256    /// for everything but `no_dheader`, so the default spelling would
257    /// call the wrong type.
258    fn bit_pattern(source: BitPatternSource, value: Self::Expr) -> Self::Expr {
259        let call = |name: &str, args: Vec<csharp::Expr>| csharp::Expr::Invoke {
260            receiver: Some(Box::new(csharp::Expr::Path(Path::rooted(&[
261                "ZeroDds", "Wire", "Writer",
262            ])))),
263            name: Ident::new(name),
264            args,
265        };
266        match source {
267            BitPatternSource::Int { octets } => call(
268                "BitPatternInt",
269                vec![
270                    value,
271                    csharp::Expr::Literal(Literal::int(i128::from(octets))),
272                ],
273            ),
274            BitPatternSource::Bool => call("BitPatternBool", vec![value]),
275            BitPatternSource::Float32 => call("BitPatternF32", vec![value]),
276            BitPatternSource::Float64 => call("BitPatternF64", vec![value]),
277        }
278    }
279
280    /// `runtime/csharp` publishes these as enum members, one type per
281    /// policy, and every reference is rooted for the reason
282    /// [`CodecTarget::handle`] gives.
283    fn constant(which: Constant) -> Self::Expr {
284        let mut segments: Vec<&str> = vec!["ZeroDds", "Wire"];
285        segments.extend(
286            match which {
287                Constant::WcharWidth2 => "WcharWidth.Octets2",
288                Constant::WcharWidth4 => "WcharWidth.Octets4",
289                Constant::WcharFixed => "WcharFraming.Fixed",
290                Constant::WcharGiopLengthPrefixed => "WcharFraming.GiopLengthPrefixed",
291                Constant::WstringUnitsStream => "WstringUnitOrder.Stream",
292                Constant::WstringUnitsBigEndianAlways => "WstringUnitOrder.BigEndianAlways",
293                Constant::WstringUnitsByteOrderMark => "WstringUnitOrder.ByteOrderMark",
294                Constant::WstringUnitsHostUnswapped => "WstringUnitOrder.HostUnswapped",
295                Constant::WstringSpecification => "WstringLengthPolicy.Specification",
296                Constant::WstringCharacterCount => "WstringLengthPolicy.CharacterCount",
297                Constant::WstringCharacterCountTerminated => {
298                    "WstringLengthPolicy.CharacterCountTerminated"
299                }
300                Constant::WstringSpecificationWideUnits => {
301                    "WstringLengthPolicy.SpecificationWideUnits"
302                }
303                Constant::WstringRosTypeSupport => "WstringLengthPolicy.RosTypeSupport",
304                Constant::WstringOctetsTerminated2 => "WstringLengthPolicy.OctetsTerminated2",
305                Constant::WstringOctetsTerminated4 => "WstringLengthPolicy.OctetsTerminated4",
306                Constant::WstringCharactersTerminated2 => {
307                    "WstringLengthPolicy.CharactersTerminated2"
308                }
309                Constant::FixedDeclared => "FixedDigitCount.Declared",
310                Constant::FixedValuePrecision => "FixedDigitCount.ValuePrecision",
311                Constant::FixedValueStringDigits => "FixedDigitCount.ValueStringDigits",
312                Constant::ParameterMember => "ParameterRole.Member",
313                Constant::ParameterOptional => "ParameterRole.Optional",
314                Constant::TryConstructDiscard => "TryConstruct.Discard",
315                Constant::TryConstructTrim => "TryConstruct.Trim",
316                Constant::TryConstructUseDefault => "TryConstruct.UseDefault",
317                Constant::StatusOk => "Status.Ok",
318                Constant::StatusDiscarded => "Status.Discarded",
319                Constant::StatusImpossibleLength => "Status.ImpossibleLength",
320                Constant::StatusOutOfRange => "Status.OutOfRange",
321                Constant::StatusTruncated => "Status.Truncated",
322            }
323            .split('.'),
324        );
325        csharp::Expr::Path(Path::rooted(&segments))
326    }
327
328    /// The materialiser's answer, not a second reading of it.
329    ///
330    /// `adapter/csharp.rs::unsigned_holder` decides what a
331    /// `@bit_bound` set's field is declared as, and Table 33's four
332    /// widths all have a C# keyword — which is where this parts from
333    /// Java, whose holder can only be `int` or `long`.
334    fn unsigned_holder(bits: u16) -> Self::Type {
335        csharp::Type::Predefined(unsigned_holder(bits))
336    }
337
338    /// C# has no free functions; the runtime class's own static method
339    /// stands in.
340    ///
341    /// Which class is not a guess: `no_dheader` is the writer's and
342    /// everything else is the reader's. A name this table does not know
343    /// is written on `Reader` and `csc` names it — a compile error that
344    /// stops the probe, rather than a call that goes to the wrong class
345    /// and cannot.
346    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
347        let receiver = if method == "no_dheader" {
348            "Writer"
349        } else {
350            "Reader"
351        };
352        csharp::Expr::Invoke {
353            receiver: Some(Box::new(csharp::Expr::Path(Path::rooted(&[
354                "ZeroDds", "Wire", receiver,
355            ])))),
356            name: Ident::new(&Self::pascal(method)),
357            args,
358        }
359    }
360
361    // -- expressions --------------------------------------------------
362
363    fn name(text: &str) -> Self::Expr {
364        csharp::Expr::Path(Self::path(text))
365    }
366
367    fn int(value: i128) -> Self::Expr {
368        csharp::Expr::Literal(Literal::int(value))
369    }
370
371    fn literal(value: Literal) -> Self::Expr {
372        csharp::Expr::Literal(value)
373    }
374
375    /// A free function is a `static` method of the generated class, so
376    /// an unqualified call reaches it from another method of the same
377    /// class without naming the class.
378    /// `strcmp` is the walk's name for a three-way code-unit
379    /// comparison; here it is `string.CompareOrdinal`, which compares
380    /// UTF-16 code units — the Java column's caveat about
381    /// supplementary code points applies unchanged.
382    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
383        if callee == "strcmp" {
384            return csharp::Expr::Invoke {
385                receiver: Some(Box::new(csharp::Expr::Path(Path::new(&["string"])))),
386                name: Ident::new("CompareOrdinal"),
387                args,
388            };
389        }
390        csharp::Expr::Invoke {
391            receiver: None,
392            name: Ident::new(callee),
393            args,
394        }
395    }
396
397    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
398        csharp::Expr::Invoke {
399            receiver: Some(Box::new(csharp::Expr::Path(Self::path(receiver)))),
400            name: Ident::new(&Self::pascal(method)),
401            args,
402        }
403    }
404
405    /// `runtime/csharp` carries the three HDDS axes: `BeginPackedElement`
406    /// and `FinishPackedElement` on the writer and the reader, the
407    /// DHEADER pair for a sequence element's own length, and
408    /// `WriteWstringUtf8Octets` / `ReadWstringUtf8Octets`.
409    fn carries_hdds_axes() -> bool {
410        true
411    }
412
413    fn member(base: &str, field: &str) -> Self::Expr {
414        csharp::Expr::Member {
415            value: Box::new(csharp::Expr::Path(Self::path(base))),
416            name: Ident::new(field),
417        }
418    }
419
420    /// The sample itself, with no dereference to write.
421    fn whole(base: &str) -> Self::Expr {
422        csharp::Expr::Path(Self::path(base))
423    }
424
425    /// The suffix that keeps a literal's type from being the
426    /// compiler's guess.
427    ///
428    /// ECMA-334 §6.4.5.3 gives an unsuffixed decimal the first of
429    /// `int`, `uint`, `long`, `ulong` that holds it, which is already
430    /// the right answer for every value — but a `long` label compared
431    /// against a `long` discriminator wants the width stated, and
432    /// `16045690984833335998` past `long.MaxValue` has to be a `ulong`
433    /// where the discriminator is one. The suffix says which, so the
434    /// comparison is not decided by the operand the compiler happened
435    /// to widen.
436    fn wide_int(value: i128) -> Self::Expr {
437        let text = if value > i128::from(i64::MAX) {
438            format!("{value}UL")
439        } else if value > i128::from(i32::MAX) || value < i128::from(i32::MIN) {
440            format!("{value}L")
441        } else {
442            format!("{value}")
443        };
444        csharp::Expr::Literal(Literal::Int(text))
445    }
446
447    /// A count, wide enough to hold itself and with no `u` suffix.
448    ///
449    /// C's `5u` is not a C# token; `5U` is, and it is not wanted: a
450    /// bound compared against an `int` counter would force the whole
451    /// comparison into `long` for no reason. The bounds past `int` get
452    /// their `L` from [`CodecTarget::wide_int`].
453    fn unsigned_int(value: i128) -> Self::Expr {
454        Self::wide_int(value)
455    }
456
457    /// A 64-bit mask, as the `ulong` it is.
458    ///
459    /// Where the Java backend has to write the signed pattern because
460    /// Java has no unsigned integer, this is the number with a `UL` on
461    /// it — and `~0xFFFFFFFFFFFFFFFFUL` is an unsigned complement
462    /// rather than an arithmetic one.
463    fn wide_unsigned(value: u64) -> Self::Expr {
464        csharp::Expr::Literal(Literal::Int(format!("{value}UL")))
465    }
466
467    /// A signed 64-bit constant, always suffixed: an enumerator's
468    /// pattern is compared against a `long` and an unsuffixed literal
469    /// there would be widened after the comparison's type was chosen.
470    fn wide_signed(value: i128) -> Self::Expr {
471        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
472        let pattern = if value > i128::from(i64::MAX) {
473            value as u64 as i64
474        } else {
475            value as i64
476        };
477        csharp::Expr::Literal(Literal::Int(format!("{pattern}L")))
478    }
479
480    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
481        csharp::Expr::Member {
482            value: Box::new(value),
483            name: Ident::new(field),
484        }
485    }
486
487    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
488        csharp::Expr::Index {
489            value: Box::new(value),
490            index: Box::new(at),
491        }
492    }
493
494    /// `(uint)v.Length`, or the bound itself where the materialiser
495    /// fixed one.
496    ///
497    /// The cast is the crossing and is written out. A C# array's
498    /// `Length` is an `int`; a sequence's count is a `uint` on the wire
499    /// and `Writer.WriteU32` takes one, and there is no implicit
500    /// `int` to `uint` conversion. A backend that let the runtime take
501    /// a wider signed type instead would be moving a range check out of
502    /// the compiler and into a throw.
503    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
504        match fixed {
505            Some(bound) => csharp::Expr::Literal(Literal::int(i128::from(bound))),
506            None => csharp::Expr::Cast {
507                type_: Box::new(csharp::Type::Predefined(csharp::Predefined::UInt)),
508                value: Box::new(csharp::Expr::Member {
509                    value: Box::new(Self::whole(base)),
510                    name: Ident::new("Length"),
511                }),
512            },
513        }
514    }
515
516    /// The operators as given, except a shift's count: C# takes only an
517    /// `int` there (§12.11 in the C# standard names no `ulong` form),
518    /// and the walk writes the count in the accumulator's own width —
519    /// `zd_bits >> 3UL` is `CS0019`. A literal count sheds its suffix,
520    /// any other spelling is cast.
521    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
522        let rhs = if matches!(op, "<<" | ">>") {
523            match rhs {
524                csharp::Expr::Literal(Literal::Int(text)) => csharp::Expr::Literal(Literal::Int(
525                    text.trim_end_matches(['L', 'U', 'l', 'u']).to_string(),
526                )),
527                other => csharp::Expr::Cast {
528                    type_: Box::new(csharp::Type::Predefined(csharp::Predefined::Int)),
529                    value: Box::new(csharp::Expr::Paren(Box::new(other))),
530                },
531            }
532        } else {
533            rhs
534        };
535        csharp::Expr::Binary {
536            op,
537            lhs: Box::new(lhs),
538            rhs: Box::new(rhs),
539        }
540    }
541
542    /// The operator as written. C# has a real unary form for `!`, `~`
543    /// and `-`, so none of them needs the `0 <op> a` stand-in the Java
544    /// backend writes.
545    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
546        csharp::Expr::Unary {
547            op,
548            value: Box::new(value),
549        }
550    }
551
552    /// A bracket, written because the walk asked for one.
553    ///
554    /// The renderer adds none of its own, and C#'s precedence is Java's
555    /// where it matters here: `&` binds looser than `!=`, so
556    /// `bits & mask != 0` is a type error rather than the comparison
557    /// the walk built.
558    fn paren(value: Self::Expr) -> Self::Expr {
559        csharp::Expr::Paren(Box::new(value))
560    }
561
562    /// A conversion, kept where Java drops it.
563    ///
564    /// Two reasons this column writes more casts than that one. A C#
565    /// `enum` has no implicit conversion to an integer, so the walk's
566    /// `(long)v` on an enumerator and `(ulong)v` on a bitmask are
567    /// load-bearing. And `csc` has no redundant-cast diagnostic, so
568    /// keeping an identity cast costs nothing where `-Xlint:cast
569    /// -Werror` fails the Java build over one.
570    ///
571    /// The one rewrite is the `bool` discriminator. A union's labels
572    /// arrive as the integers the IDL evaluator gives, and C# has no
573    /// conversion from `1` to `true`; the label is the value it stands
574    /// for.
575    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
576        if type_ == csharp::Type::Predefined(csharp::Predefined::Bool) {
577            if let csharp::Expr::Literal(Literal::Int(text)) = &value {
578                return csharp::Expr::Literal(Literal::Bool(
579                    text.trim_end_matches(['L', 'U', 'l', 'u']) != "0",
580                ));
581            }
582        }
583        // The walk's `signed char` — the C spelling of a map key's
584        // signed order — is `sbyte` here: the cast keeps the low octet
585        // and reads it in two's complement, which is the order the
586        // corpus was recorded under.
587        if type_ == csharp::Type::named(Self::path("signed char")) {
588            return csharp::Expr::Cast {
589                type_: Box::new(csharp::Type::Predefined(csharp::Predefined::SByte)),
590                value: Box::new(value),
591            };
592        }
593        csharp::Expr::Cast {
594            type_: Box::new(type_),
595            value: Box::new(value),
596        }
597    }
598
599    /// `false` before `true`, spelled as the conjunction — C#'s `<`
600    /// does not take two booleans (`CS0019`).
601    fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
602        Self::binary(
603            "&&",
604            csharp::Expr::Paren(Box::new(csharp::Expr::Binary {
605                op: "==",
606                lhs: Box::new(left),
607                rhs: Box::new(csharp::Expr::Literal(Literal::Bool(false))),
608            })),
609            right,
610        )
611    }
612
613    /// Nothing to take an address of and no array to decay: both forms
614    /// hand the value.
615    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
616        value
617    }
618
619    // -- statements ---------------------------------------------------
620
621    /// A local, with `= default` where the walk asked for no value.
622    ///
623    /// The walk declares a header handle before the loop that fills it
624    /// — `EmHeaderRead zd_h;` — and C#'s definite-assignment rules make
625    /// a local read before its first assignment a compile error, which
626    /// a loop body the compiler cannot prove runs would be. `default`
627    /// on a `readonly struct` costs nothing and settles it.
628    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
629        csharp::Statement::Declare {
630            type_,
631            name: Ident::new(name),
632            value: Some(value.unwrap_or(csharp::Expr::Default(None))),
633        }
634    }
635
636    fn perform(value: Self::Expr) -> Self::Stmt {
637        csharp::Statement::Expr(value)
638    }
639
640    /// `return v;` where the walk asked for a bare `return`.
641    ///
642    /// Only the reader leaves a routine early, and a C# decoder answers
643    /// with the sample it has built rather than writing through a
644    /// parameter it would have to take by `ref`. A bare `return;` would
645    /// leave the caller with whatever it had before, which is the
646    /// silent-wrong-value shape.
647    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
648        csharp::Statement::Return(Some(value.unwrap_or_else(|| Self::whole("v"))))
649    }
650
651    /// `_ = name;`, a discard (C# 7.0): a read the language accepts as
652    /// a statement, where `name;` is not one. `csc` has no unread-parameter
653    /// diagnostic, but a local that is assigned and never read is CS0219,
654    /// and the probe treats warnings as errors: under OpenDDS's XCDR1
655    /// framing a union writes no sentinel and nothing reads `zd_sent`
656    /// (`union-mutable`, `evolution-union-unselected`, 4 matrix cells).
657    fn mark_used(name: &str) -> Vec<Self::Stmt> {
658        vec![csharp::Statement::Expr(csharp::Expr::Assign {
659            target: Box::new(Self::name("_")),
660            value: Box::new(Self::name(name)),
661        })]
662    }
663
664    /// The call on its own. An invocation is already a C# statement,
665    /// and `(void) expr;` is not one at all.
666    fn drop_value(value: Self::Expr) -> Self::Stmt {
667        csharp::Statement::Expr(value)
668    }
669
670    /// `place = zd_dec_T(r, behaviour);`
671    ///
672    /// A decoder answers with its value rather than filling a
673    /// destination. C# has `ref` and `out`, and neither is usable here:
674    /// a property cannot be passed as either (CS0206), an array element
675    /// of a `Nullable<T>` member cannot be an `out` of `T`, and the
676    /// walk's destinations are expressions rather than variables.
677    fn read_call(
678        callee: &str,
679        place: Self::Expr,
680        _form: Handoff,
681        behaviour: Self::Expr,
682    ) -> Self::Stmt {
683        csharp::Statement::Expr(csharp::Expr::Binary {
684            op: "=",
685            lhs: Box::new(place),
686            rhs: Box::new(Self::call(callee, vec![Self::name("r"), behaviour])),
687        })
688    }
689
690    /// `v = zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
691    fn read_forward(callee: &str) -> Self::Stmt {
692        Self::read_call(
693            callee,
694            Self::whole("v"),
695            Handoff::Value,
696            Self::name("zd_tc"),
697        )
698    }
699
700    /// A shorter array, because a C# array's length is fixed at its
701    /// allocation.
702    ///
703    /// §7.3.1.2.1.12.3's TRIM. C leaves the elements past the bound in
704    /// the buffer and moves the count, and there is no count here to
705    /// move; `Array.Resize` takes a `ref` and the walk's base is an
706    /// expression, so the shortening is a copy. Written through
707    /// `System.Linq` rather than as three statements with a temporary,
708    /// because a temporary would need a name no two calls in one method
709    /// could share.
710    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
711        let take = csharp::Expr::Invoke {
712            receiver: Some(Box::new(csharp::Expr::Path(Path::rooted(&[
713                "System",
714                "Linq",
715                "Enumerable",
716            ])))),
717            name: Ident::new("Take"),
718            args: vec![
719                Self::whole(base),
720                csharp::Expr::Cast {
721                    type_: Box::new(csharp::Type::Predefined(csharp::Predefined::Int)),
722                    value: Box::new(count),
723                },
724            ],
725        };
726        vec![csharp::Statement::Expr(csharp::Expr::Binary {
727            op: "=",
728            lhs: Box::new(Self::whole(base)),
729            rhs: Box::new(csharp::Expr::Invoke {
730                receiver: Some(Box::new(csharp::Expr::Path(Path::rooted(&[
731                    "System",
732                    "Linq",
733                    "Enumerable",
734                ])))),
735                name: Ident::new("ToArray"),
736                args: vec![take],
737            }),
738        })]
739    }
740
741    fn when(
742        condition: Self::Expr,
743        then: Self::Block,
744        otherwise: Option<Self::Block>,
745    ) -> Self::Stmt {
746        csharp::Statement::If {
747            condition,
748            then,
749            otherwise,
750        }
751    }
752
753    /// `for (int i = 0; i < bound; i = i + 1)`.
754    ///
755    /// The step is written out rather than `i++` for the same reason
756    /// the C and Java backends write it out: the tree has no increment
757    /// node, and one spelling in every backend keeps the columns'
758    /// generated source comparable by eye when their octets disagree.
759    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
760        csharp::Statement::For {
761            init: Box::new(csharp::Statement::Declare {
762                type_: csharp::Type::Predefined(csharp::Predefined::Int),
763                name: Ident::new(counter),
764                value: Some(csharp::Expr::Literal(Literal::int(0))),
765            }),
766            condition: csharp::Expr::Binary {
767                op: "<",
768                lhs: Box::new(csharp::Expr::Path(Path::new(&[counter]))),
769                rhs: Box::new(bound),
770            },
771            step: csharp::Expr::Binary {
772                op: "=",
773                lhs: Box::new(csharp::Expr::Path(Path::new(&[counter]))),
774                rhs: Box::new(csharp::Expr::Binary {
775                    op: "+",
776                    lhs: Box::new(csharp::Expr::Path(Path::new(&[counter]))),
777                    rhs: Box::new(csharp::Expr::Literal(Literal::int(1))),
778                }),
779            },
780            body,
781        }
782    }
783
784    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
785        csharp::Statement::While { condition, body }
786    }
787
788    fn leave() -> Self::Stmt {
789        csharp::Statement::Break
790    }
791
792    fn note(doc: Doc) -> Self::Stmt {
793        csharp::Statement::Comment(doc)
794    }
795
796    // -- assembly -----------------------------------------------------
797
798    /// The array, sized — and its slots given something to be where the
799    /// fill writes into them rather than assigning them.
800    ///
801    /// `new T[n]` throws rather than returning null when it cannot be
802    /// served, so there is no guard and the fill follows
803    /// unconditionally. `new T[n]` takes the count as an `int`, a
804    /// `uint`, a `long` or a `ulong`, so the `uint` the reader read is
805    /// handed over with no cast at all.
806    ///
807    /// `new T[n]` of a class type is `n` nulls. A sequence's element
808    /// decoder answers with a value and the slot is assigned outright,
809    /// so nothing is needed; a `map`'s two halves are written into the
810    /// entry at `v[i]`, and an entry that was never made is a null
811    /// dereference at the first key. That is what `assigned_whole`
812    /// separates.
813    fn make_room(
814        base: &str,
815        element: Self::Type,
816        count: Self::Expr,
817        assigned_whole: bool,
818        fill: Vec<Self::Stmt>,
819    ) -> Vec<Self::Stmt> {
820        let (innermost, nested) = Self::peel(element.clone());
821        let mut out = vec![csharp::Statement::Expr(csharp::Expr::Binary {
822            op: "=",
823            lhs: Box::new(Self::whole(base)),
824            rhs: Box::new(csharp::Expr::NewArray {
825                element: Box::new(innermost),
826                length: Some(Box::new(count)),
827                elements: Vec::new(),
828                nested,
829            }),
830        })];
831        if !assigned_whole {
832            out.push(Self::count_up(
833                "zd_e",
834                Self::count_of(base, None),
835                csharp::Block {
836                    statements: vec![csharp::Statement::Expr(csharp::Expr::Binary {
837                        op: "=",
838                        lhs: Box::new(csharp::Expr::Index {
839                            value: Box::new(Self::whole(base)),
840                            index: Box::new(Self::name("zd_e")),
841                        }),
842                        rhs: Box::new(csharp::Expr::New {
843                            type_: Box::new(element),
844                            args: Vec::new(),
845                            initialiser: Vec::new(),
846                        }),
847                    })],
848                },
849            ));
850        }
851        out.extend(fill);
852        out
853    }
854
855    /// The scalar into the place, cast to the place's own type.
856    ///
857    /// One cast and never two: the walk has already asked for a width
858    /// at one site — the element form of a `wchar` under TAO's framing
859    /// asks for `ushort` — and a `ushort` does not convert to a `char`
860    /// on its own. The outer cast is unwrapped and rewritten to the
861    /// field's type, which is the same thing the Java backend does for
862    /// its three narrow holders.
863    ///
864    /// A `Float128` place takes the value unchanged: it is a generated
865    /// struct and a cast to it does not compile.
866    fn store(place: Self::Expr, type_: Self::Type, value: Self::Expr) -> Self::Stmt {
867        if !Self::is_castable(&type_) {
868            return csharp::Statement::Expr(csharp::Expr::Binary {
869                op: "=",
870                lhs: Box::new(place),
871                rhs: Box::new(value),
872            });
873        }
874        let inner = match value {
875            csharp::Expr::Cast { value, .. } => *value,
876            other => other,
877        };
878        csharp::Statement::Expr(csharp::Expr::Binary {
879            op: "=",
880            lhs: Box::new(place),
881            rhs: Box::new(csharp::Expr::Cast {
882                type_: Box::new(type_),
883                value: Box::new(inner),
884            }),
885        })
886    }
887
888    /// `<T> name = new <T>();` — a C# local of a class type is a
889    /// reference to nothing until one is made.
890    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
891        csharp::Statement::Declare {
892            type_: type_.clone(),
893            name: Ident::new(name),
894            value: Some(csharp::Expr::New {
895                type_: Box::new(type_),
896                args: Vec::new(),
897                initialiser: Vec::new(),
898            }),
899        }
900    }
901
902    /// `runtime/csharp` publishes a header's parts as fields and
903    /// properties in .NET's naming, so the field the walk names is
904    /// re-cased here.
905    ///
906    /// A `match` and not a general re-casing: `end` is `End` and
907    /// `extent` is a computed property, so a rule that only capitalised
908    /// would name two members that are not there. A name this table
909    /// does not know is written as it came, and `csc` reports it.
910    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
911        csharp::Expr::Member {
912            value: Box::new(value),
913            name: Ident::new(match field {
914                "pid" => "Pid",
915                "end" => "End",
916                "ignore" => "Ignore",
917                "length" => "Length",
918                "member_id" => "MemberId",
919                "must_understand" => "MustUnderstand",
920                "extent" => "Extent",
921                other => other,
922            }),
923        }
924    }
925
926    /// Absence is the value, so presence is a comparison and not a
927    /// second field. `null` works for both halves of the mapping: a
928    /// reference type is nullable already and a value type's `T?` is a
929    /// `Nullable<T>`, which compares against `null`.
930    fn present(base: &str, field: &str) -> Self::Expr {
931        csharp::Expr::Binary {
932            op: "!=",
933            lhs: Box::new(Self::member(base, field)),
934            rhs: Box::new(Self::absent()),
935        }
936    }
937
938    /// Nothing on the way in — assigning the member is what makes it
939    /// present — and `null` on the way out.
940    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
941        if present {
942            return Vec::new();
943        }
944        vec![csharp::Statement::Expr(csharp::Expr::Binary {
945            op: "=",
946            lhs: Box::new(Self::member(base, field)),
947            rhs: Box::new(Self::absent()),
948        })]
949    }
950
951    /// One field per case, directly on the class.
952    fn case_member(base: &str, field: &str) -> Self::Expr {
953        Self::member(base, field)
954    }
955
956    /// `Key` and `Value`, which is what `fields_of` folded the entry
957    /// class's two IDL names to.
958    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
959        csharp::Expr::Member {
960            value: Box::new(entry),
961            name: Ident::new(if key { "Key" } else { "Value" }),
962        }
963    }
964
965    /// The member, unwrapped where the mapping wrapped it.
966    ///
967    /// A value type's `@optional` member is a `Nullable<T>` and does
968    /// not convert to `T` on its own, so the write side takes
969    /// `.Value` — reached only inside the branch the presence test
970    /// guarded, which is where `Nullable<T>.Value` is defined. A
971    /// reference type was never wrapped and is handed over as it is.
972    fn optional_value(ast: &Ast, id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
973        if crate::adapter::csharp::is_nullable_wrapped(ast, id) {
974            return csharp::Expr::Member {
975                value: Box::new(Self::member(base, field)),
976                name: Ident::new("Value"),
977            };
978        }
979        Self::member(base, field)
980    }
981
982    /// `v[i]`, and nothing around it: `adapter/csharp.rs` gives a
983    /// sequence a `T[]`, whose elements are real slots at every element
984    /// type.
985    fn sequence_element(
986        base: &str,
987        index: Self::Expr,
988        _element: Self::Type,
989    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
990        (
991            Vec::new(),
992            Self::index(Self::elements(base), index),
993            Vec::new(),
994        )
995    }
996
997    /// The member itself. Assigning to it is what makes it present, and
998    /// a `T` assigned to a `T?` is the implicit conversion C# already
999    /// has, so there is nothing to do first.
1000    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
1001        (Vec::new(), Self::member(base, field))
1002    }
1003
1004    fn elements(base: &str) -> Self::Expr {
1005        Self::whole(base)
1006    }
1007
1008    fn absent() -> Self::Expr {
1009        csharp::Expr::Literal(Literal::Null)
1010    }
1011
1012    /// `!= null`, because a `string` and a `T[]` are references here.
1013    fn materialised(value: Self::Expr) -> Option<Self::Expr> {
1014        Some(csharp::Expr::Binary {
1015            op: "!=",
1016            lhs: Box::new(value),
1017            rhs: Box::new(Self::absent()),
1018        })
1019    }
1020
1021    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1022        csharp::Block { statements }
1023    }
1024
1025    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1026        block.statements
1027    }
1028
1029    /// `private static void EncodeT(Writer w, T v)`.
1030    ///
1031    /// A prototype gets `None`: a C# method may call one declared below
1032    /// it, so there is nothing to declare ahead. `tolerate_unused` is
1033    /// dropped for the same kind of reason — `csc` does not warn on an
1034    /// uncalled private method under the settings the probe builds
1035    /// with, so there is no warning to suppress.
1036    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1037        let body = spec.body?;
1038        let mut params = vec![csharp::Param {
1039            attributes: Vec::new(),
1040            modifier: None,
1041            type_: csharp::Type::named(Path::rooted(&[
1042                "ZeroDds",
1043                "Wire",
1044                if spec.writing { "Writer" } else { "Reader" },
1045            ])),
1046            name: Ident::new(if spec.writing { "w" } else { "r" }),
1047            default: None,
1048        }];
1049        // The writer is handed the sample; the reader answers with one.
1050        // See `CodecTarget::read_call`.
1051        if spec.writing {
1052            params.push(csharp::Param {
1053                attributes: Vec::new(),
1054                modifier: None,
1055                type_: spec.sample.clone(),
1056                name: Ident::new("v"),
1057                default: None,
1058            });
1059        }
1060        params.extend(spec.extra.into_iter().map(|(type_, name)| csharp::Param {
1061            attributes: Vec::new(),
1062            modifier: None,
1063            type_,
1064            name: Ident::new(&name),
1065            default: None,
1066        }));
1067        let (returns, body) = if spec.writing {
1068            (Self::nothing(), body)
1069        } else {
1070            (
1071                spec.sample.clone(),
1072                Self::reading_body(&spec.name, spec.sample, body),
1073            )
1074        };
1075        Some(csharp::Member::Method(csharp::Method {
1076            doc: spec.doc,
1077            attributes: Vec::new(),
1078            // `public` and not `private`. `ZdCodec` is a `public
1079            // static class` and a generated codec's whole purpose is to
1080            // be called from the application that asked for it; every
1081            // method in it was `private`, so an assembly holding this
1082            // file could not call one. The probe harness never noticed:
1083            // it emits its `Main` into the same class.
1084            modifiers: vec![csharp::Modifier::Public, csharp::Modifier::Static],
1085            returns,
1086            name: Ident::new(&spec.name),
1087            generics: Vec::new(),
1088            params,
1089            constraints: Vec::new(),
1090            body: Some(body),
1091            expression: None,
1092        }))
1093    }
1094
1095    // -- the materialiser's boundary ----------------------------------
1096
1097    /// The materialiser's spelling, rooted: `global::a.b.Point`.
1098    ///
1099    /// Not [`CodecTarget::type_name`], which is the codec's tag —
1100    /// `a_b_Point` — and names a method rather than a type.
1101    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
1102        csharp_member_type(ast, id, "codec").unwrap_or_else(|_| {
1103            csharp::Type::named(Path::new(
1104                &[format!("UnresolvedType{}", id.slot()).as_str()],
1105            ))
1106        })
1107    }
1108
1109    /// The C# materialiser's tag for a type.
1110    ///
1111    /// The fallback carries the arena slot, and that is the point of
1112    /// it: `type_tag`'s own answer for an unresolvable type is the bare
1113    /// word `unknown`, and two of those in one model would be two types
1114    /// sharing one codec — a member written by the wrong encoder, with
1115    /// nothing red anywhere.
1116    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
1117        type_tag(ast, id, 0).unwrap_or_else(|_| format!("unresolved_type_{}", id.slot()))
1118    }
1119
1120    /// The member fold, and only that.
1121    ///
1122    /// `venue` to `Venue`, with a C# keyword escaped to `@venue`. Not
1123    /// an enumerator's name, which the materialiser leaves verbatim —
1124    /// [`CodecTarget::enumerator_constant`] is that seam.
1125    fn escape(name: &str) -> String {
1126        field_name(name)
1127    }
1128
1129    /// `global::ZdEntry_<key>_<value>`.
1130    ///
1131    /// The entry class is declared at the global scope and every
1132    /// reference this column writes is rooted, for the reason
1133    /// `adapter/csharp.rs` gives: a nested namespace can capture the
1134    /// first segment of an unrooted path.
1135    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
1136        map_entry_name(ast, id).map_or_else(
1137            |_| format!("UnresolvedEntry{}", id.slot()),
1138            |name| format!("global::{name}"),
1139        )
1140    }
1141
1142    /// C# has namespaces, so a module needs no prefix in a name.
1143    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
1144        String::new()
1145    }
1146
1147    /// The member's own `@default`, or Table 9's value for its type.
1148    ///
1149    /// # Errors
1150    /// As [`crate::default_csharp::member_default`].
1151    fn member_default(
1152        ast: &Ast,
1153        node: &MemberNode,
1154        type_: Id<TypeNode>,
1155        place: Self::Expr,
1156    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1157        crate::default_csharp::member_default(ast, node, type_, place)
1158    }
1159
1160    /// `place = zd_def_<T>();` — Table 9, through the generated
1161    /// defaulter.
1162    ///
1163    /// # Errors
1164    /// Never here. Whether the type *has* a C# default is decided where
1165    /// the defaulter is built, and refused there by name.
1166    fn default_into(
1167        ast: &Ast,
1168        id: Id<TypeNode>,
1169        place: Self::Expr,
1170    ) -> Result<Self::Stmt, EncodeError> {
1171        Ok(crate::default_csharp::default_into(ast, id, place))
1172    }
1173
1174    /// `global::<modules>.<Enum>.<LITERAL>`.
1175    ///
1176    /// The materialiser declares a real `enum` and leaves the
1177    /// enumerator's name verbatim; a bare `RED` names nothing outside
1178    /// the enum's own scope, and `Self::escape` would fold it into a
1179    /// member name the enum does not have.
1180    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1181        crate::default_csharp::enumerator_constant(ast, id, enumerator)
1182    }
1183}
1184
1185/// Where the C# materialiser puts a declaration, as a rooted name.
1186///
1187/// Re-exported so the probe generator can name a class without a second
1188/// derivation of the module chain.
1189///
1190/// # Errors
1191/// As [`declared_path`].
1192pub fn declared_type_path(
1193    ast: &Ast,
1194    id: Id<TypeNode>,
1195) -> Result<String, crate::adapter::cst::MaterialiseError> {
1196    declared_path(ast, id)
1197}