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}