Skip to main content

zerodds_lang_rules/
default_csharp.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value a C# member takes when the stream does not carry it.
4//!
5//! Table 9 of DDS-XTypes 1.3, as C#. [`crate::default_c`] is the same
6//! table as C, [`crate::default_java`] as Java and
7//! `crates/wire-probe/src/default.rs` as the reflective column; that
8//! module carries the clause quotations and the reasoning for the three
9//! kinds Table 9 does not list. Deliberately a fifth derivation and not
10//! a call into any of them: the columns are compared against each
11//! other, and a shared helper would make them agree about a mistake.
12//!
13//! ## Why it is a generated method and not a literal
14//!
15//! A literal cannot carry an aggregate. "A structure without any of the
16//! optional members and with other members set to their default values"
17//! is a `new S()` and one assignment per member, and writing that at
18//! every position a member can go missing would put the same answer in
19//! the generated file as many times as the type is reached — where two
20//! copies can differ.
21//!
22//! ## Why it answers with the value instead of filling one
23//!
24//! C's defaulter is `void zd_def_T(T *v)`: it writes through a pointer.
25//! C# has `ref` and `out` and neither is usable at the call sites the
26//! walk produces — a property cannot be passed as either (CS0206), and
27//! a `Nullable<T>` member is not an `out T`. So the method **returns**
28//! Table 9's value and the caller assigns it, which is the same decision
29//! one step later, and it is what makes the reader's shape work: every
30//! generated decoder opens with `T v = zd_def_T();`, so a position the
31//! stream does not carry is already what Table 9 says before a single
32//! octet is read.
33//!
34//! ## What C# settles that Java could not
35//!
36//! Every zero row of Table 9 is spelled by the member's own width,
37//! because the mapping is one to one — an `octet` is a `byte` and an
38//! `unsigned long` is a `uint`, where the Java column has to write both
39//! as the next signed type up. What is left is the two rows C# spells
40//! differently from every other column: a `float` needs its `f` suffix
41//! because a real literal is a `double` and there is no implicit
42//! narrowing even for a constant, and an enumeration's default is a real
43//! `enum` member rather than an `int`.
44//!
45//! A `sequence<T>` is a `T[]` and its "zero-length sequence" is
46//! `new T[0]` and not a null: null is what an `@optional` member's
47//! absence means here, and one value cannot mean both. §7.2.2.4.4.4.7
48//! asks for a zero-length sequence, and §7.2.2.4.4.4.8's
49//! "Implementations shall not provide a default value" for an
50//! `@optional` member is the null.
51
52use crate::adapter::csharp::{csharp_member_type, declared_path, field_name, type_tag};
53use crate::adapter::cst::MaterialiseError;
54use crate::default_common::{discriminator_default_value, resolve_alias, selected_case};
55use crate::emit::csharp::CSharp;
56use crate::encode::EncodeError;
57use crate::fixed_literal::{fixed_text, zero_decimal};
58use zerodds_ast::{
59    Ast, ConstExpr, Literal as AstLiteral, MemberNode, PrimitiveKind, TypeKind, TypeNode,
60};
61use zerodds_ast_core::Id;
62use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
63use zerodds_lang_cst::csharp;
64
65/// The defaulter method name for a type.
66///
67/// Keyed the same way [`crate::emit::decode::decoder_name`] is, so a
68/// primitive's defaulter is named by its IDL kind: `double` and
69/// `long double` share no C# type at all, and `char` and `wchar` share
70/// one.
71#[must_use]
72pub fn defaulter_name(ast: &Ast, id: Id<TypeNode>) -> String {
73    match ast.type_(id).map(|node| node.kind.clone()) {
74        Ok(TypeKind::Primitive(kind)) => {
75            format!("zd_def_p_{}", kind.as_str().replace(' ', "_"))
76        }
77        _ => format!(
78            "zd_def_{}",
79            type_tag(ast, id, 0).unwrap_or_else(|_| format!("unresolved_type_{}", id.slot()))
80        ),
81    }
82}
83
84/// The defaulter that goes with a decoder, by name.
85///
86/// The two schemes differ in one word — `zd_dec_p_long` against
87/// `zd_def_p_long` — because both are built from the same key. Doing it
88/// by text lets `crate::emit::csharp::CSharp::routine` name the
89/// defaulter without carrying the model into a method that has only a
90/// signature; a name that does not resolve is a `csc` error naming the
91/// method.
92#[must_use]
93pub fn defaulter_from_decoder(decoder: &str) -> String {
94    match decoder.strip_prefix("zd_dec_") {
95        Some(rest) => format!("zd_def_{rest}"),
96        None => format!("zd_def_{decoder}"),
97    }
98}
99
100/// `place = zd_def_<T>();`
101pub fn default_into(ast: &Ast, id: Id<TypeNode>, place: csharp::Expr) -> csharp::Statement {
102    assign(place, call(&defaulter_name(ast, id), Vec::new()))
103}
104
105/// One member's default: its `@default`, or its type's.
106///
107/// §7.2.2.4.4.4.9 puts the explicit one first — "An explicit definition
108/// associates a default value with the member at the time where the
109/// aggregated type is defined" — and falls back to "the default value of
110/// the member type. See Table 9."
111///
112/// # Errors
113/// [`EncodeError::NoWriter`] where a `@default` names a value this
114/// column cannot spell as a C# expression. Refused rather than silently
115/// replaced by Table 9's zero: a member the designer gave a default and
116/// a member that got a zero instead are different samples.
117pub fn member_default(
118    ast: &Ast,
119    node: &MemberNode,
120    type_: Id<TypeNode>,
121    place: csharp::Expr,
122) -> Result<Vec<csharp::Statement>, EncodeError> {
123    let Some(expr) = node.facets.xtypes.as_ref().and_then(|x| x.default_value) else {
124        return Ok(vec![default_into(ast, type_, place)]);
125    };
126    let value = explicit(ast, expr, type_, &node.name)?;
127    Ok(vec![assign(place, value)])
128}
129
130/// The defaulters `members` call, and exactly those.
131///
132/// The set is read **off the emitted calls** rather than derived a
133/// second time from the model, for the reason
134/// [`crate::default_c::c_defaulters_called_by`] gives at length: a
135/// derivation beside the decoder's is a second reading of §7.2.2.7, and
136/// the two readings drift. The closure is taken by emitting and
137/// re-reading until the batch calls nothing it does not also define.
138///
139/// # Errors
140/// [`EncodeError`] for a construct with no C# default.
141pub fn csharp_defaulters_called_by(
142    ast: &Ast,
143    members: &[csharp::Member],
144) -> Result<Vec<csharp::Member>, EncodeError> {
145    let index: Vec<(String, Id<TypeNode>)> = ast
146        .types()
147        .map(|(id, _)| (defaulter_name(ast, id), id))
148        .collect();
149    let mut called: Vec<String> = Vec::new();
150    for member in members {
151        calls_in_member(member, &mut called);
152    }
153    let mut wanted: Vec<Id<TypeNode>> = Vec::new();
154    let mut have: Vec<String> = Vec::new();
155    // Bounded by the number of named defaulters, because every round
156    // but the last adds at least one. Written down rather than left to
157    // the loop, so a name that never resolves cannot spin.
158    for _ in 0..=index.len() {
159        let mut grew = false;
160        for name in &called {
161            if have.contains(name) {
162                continue;
163            }
164            have.push(name.clone());
165            if let Some(id) = index.iter().find(|(n, _)| n == name).map(|(_, id)| *id) {
166                wanted.push(id);
167                grew = true;
168            }
169        }
170        let produced = csharp_defaulters_over(ast, &wanted)?;
171        if !grew {
172            return Ok(produced);
173        }
174        for member in &produced {
175            calls_in_member(member, &mut called);
176        }
177    }
178    csharp_defaulters_over(ast, &wanted)
179}
180
181/// Build the defaulter for each of `types`, once per name.
182///
183/// # Errors
184/// [`EncodeError`] for a construct with no C# default.
185pub fn csharp_defaulters_over(
186    ast: &Ast,
187    types: &[Id<TypeNode>],
188) -> Result<Vec<csharp::Member>, EncodeError> {
189    let mut out = Vec::new();
190    let mut emitted: Vec<String> = Vec::new();
191    for id in types.iter().copied() {
192        let name = defaulter_name(ast, id);
193        if emitted.contains(&name) {
194            continue;
195        }
196        let Some(body) = body_of(ast, id, &name)? else {
197            continue;
198        };
199        emitted.push(name.clone());
200        out.push(csharp::Member::Method(csharp::Method {
201            doc: Doc::line(
202                "Table 9 of DDS-XTypes 1.3: what this type is where the stream did not \
203                 carry it.",
204            ),
205            attributes: Vec::new(),
206            modifiers: vec![csharp::Modifier::Private, csharp::Modifier::Static],
207            returns: value_type(ast, id),
208            name: Ident::new(&name),
209            generics: Vec::new(),
210            params: Vec::new(),
211            constraints: Vec::new(),
212            body: Some(csharp::Block { statements: body }),
213            expression: None,
214        }));
215    }
216    Ok(out)
217}
218
219/// The C# type a defaulter answers with.
220fn value_type(ast: &Ast, id: Id<TypeNode>) -> csharp::Type {
221    csharp_member_type(ast, id, "default").unwrap_or_else(|_| {
222        csharp::Type::named(Path::new(
223            &[format!("UnresolvedType{}", id.slot()).as_str()],
224        ))
225    })
226}
227
228/// The statements of one defaulter, or `None` where the type has no C#
229/// data form at all.
230#[allow(clippy::too_many_lines)]
231fn body_of(
232    ast: &Ast,
233    id: Id<TypeNode>,
234    own_name: &str,
235) -> Result<Option<Vec<csharp::Statement>>, EncodeError> {
236    let refuse = |kind: &'static str| EncodeError::NoWriter {
237        member: own_name.to_string(),
238        kind,
239    };
240    // A type the model does not hold. It used to answer `Ok(None)`,
241    // which the caller reads as "nothing to emit for this one" and
242    // skips — so a type whose handle no longer resolves quietly loses
243    // its defaulter, and the first sign of it is a member left at
244    // whatever the language zero-initialises to. Every other arm of
245    // this function either answers a value or refuses by name; this
246    // one now does too.
247    let Ok(node) = ast.type_(id) else {
248        return Err(refuse("a type the model does not hold"));
249    };
250    let statements = match &node.kind {
251        TypeKind::Primitive(kind) => vec![give_back(primitive_default(*kind))],
252
253        // `""`, for both widths: a .NET `string` is UTF-16 already and
254        // the empty string is the same string in either.
255        TypeKind::String { .. } => vec![give_back(text(""))],
256
257        // A `fixed<P,S>` is the decimal it was written as, and one
258        // scale has exactly one spelling of zero.
259        TypeKind::Fixed { scale, .. } => vec![give_back(text(&zero_decimal(*scale)))],
260
261        TypeKind::Alias { aliased } => {
262            let Some(target) = aliased.resolved() else {
263                return Err(refuse("typedef with an unresolved target"));
264            };
265            vec![give_back(call(&defaulter_name(ast, target), Vec::new()))]
266        }
267
268        // "The first literal in the enumeration", or the one
269        // `@default_literal` names — §7.3.1.2.1.10.
270        TypeKind::Enumeration { .. } => {
271            let Some(enumerator) = crate::emit::default_enumerator_name(ast, id) else {
272                return Err(refuse(
273                    "enumeration with no enumerator to take as its default",
274                ));
275            };
276            vec![give_back(enumerator_constant(ast, id, &enumerator))]
277        }
278
279        // No bit set: the holder's Table 9 default is zero and zero sets
280        // none. A cast and not a bare `0`: a C# `[Flags]` enum has no
281        // implicit conversion from an integer, and `default` would be
282        // the same value written in a way that does not say which type
283        // it is.
284        TypeKind::Bitmask { .. } => vec![give_back(csharp::Expr::Cast {
285            type_: Box::new(value_type(ast, id)),
286            value: Box::new(csharp::Expr::Literal(Literal::int(0))),
287        })],
288
289        TypeKind::Bitset { .. } => {
290            let mut statements = vec![new_local(value_type(ast, id))];
291            for field in &ast.all_bitfields_of(id) {
292                let Ok(field_node) = ast.bitfield(*field) else {
293                    continue;
294                };
295                // An anonymous bitfield is padding and holds no value.
296                if let Some(name) = field_node.name.as_ref() {
297                    statements.push(assign(
298                        member("v", &field_name(name)),
299                        csharp::Expr::Literal(Literal::int(0)),
300                    ));
301                }
302            }
303            statements.push(give_back(name("v")));
304            statements
305        }
306
307        // "A zero-length sequence of the same element type", and the
308        // same for a map's entries.
309        TypeKind::Sequence { .. } | TypeKind::Map { .. } => {
310            let csharp::Type::Array { .. } = value_type(ast, id) else {
311                return Err(refuse("collection whose C# form is not an array"));
312            };
313            let (element, nested) = CSharp::peel(value_type(ast, id));
314            vec![give_back(csharp::Expr::NewArray {
315                element: Box::new(element),
316                length: Some(Box::new(csharp::Expr::Literal(Literal::int(0)))),
317                elements: Vec::new(),
318                nested: nested.saturating_sub(1),
319            })]
320        }
321
322        // "An array of the same dimensions and same element type whose
323        // elements take the default value for their corresponding
324        // type." A C# multi-rank IDL array is jagged — one `[]` per
325        // dimension — so every rank but the outermost is allocated
326        // inside the loop above it. `new T[a][b]` is not an expression
327        // C# has.
328        TypeKind::Array { element, dims } => {
329            let Some(element) = element.resolved() else {
330                return Err(refuse("array with an unresolved element"));
331            };
332            let mut lengths = Vec::new();
333            for dim in dims {
334                lengths.push(
335                    ast.eval_integral(*dim)
336                        .map_err(|_| refuse("array whose length does not evaluate"))?,
337                );
338            }
339            // `total` is every `[]` the C# type carries and `ranks` is
340            // only the IDL array's own: `long rows[3]` where the element
341            // is a `sequence<long>` is an `int[][]`, so the outermost
342            // allocation is `new int[3][]` and the element's own rank
343            // comes from the element and not from the dimension list.
344            // Counting `ranks` alone wrote `new int[3]` into an
345            // `int[][]` and `csc` reported it — four corpus cases,
346            // `array-1d-sequence` and `array-1d-map` with their
347            // unannotated twins.
348            let (innermost, total) = CSharp::peel(value_type(ast, id));
349            let ranks = lengths.len();
350            let mut statements = vec![csharp::Statement::Declare {
351                type_: value_type(ast, id),
352                name: Ident::new("v"),
353                value: Some(csharp::Expr::NewArray {
354                    element: Box::new(innermost.clone()),
355                    length: Some(Box::new(csharp::Expr::Literal(Literal::int(lengths[0])))),
356                    elements: Vec::new(),
357                    nested: total - 1,
358                }),
359            }];
360
361            // The body, built innermost first: at the deepest level the
362            // element takes Table 9's value, and at every level above it
363            // the next rank is allocated before the loop that fills it.
364            let place = |depth: usize| {
365                let mut access = name("v");
366                for level in 0..=depth {
367                    access = csharp::Expr::Index {
368                        value: Box::new(access),
369                        index: Box::new(name(&format!("zd_i{level}"))),
370                    };
371                }
372                access
373            };
374            let mut inner = vec![default_into(ast, element, place(ranks - 1))];
375            for level in (0..ranks).rev() {
376                let mut body = Vec::new();
377                if level + 1 < ranks {
378                    body.push(assign(
379                        place(level),
380                        csharp::Expr::NewArray {
381                            element: Box::new(innermost.clone()),
382                            length: Some(Box::new(csharp::Expr::Literal(Literal::int(
383                                lengths[level + 1],
384                            )))),
385                            elements: Vec::new(),
386                            nested: total - level - 2,
387                        },
388                    ));
389                }
390                body.extend(inner);
391                inner = vec![counted_loop(
392                    &format!("zd_i{level}"),
393                    csharp::Expr::Literal(Literal::int(lengths[level])),
394                    csharp::Block { statements: body },
395                )];
396            }
397            statements.extend(inner);
398            statements.push(give_back(name("v")));
399            statements
400        }
401
402        // "A structure without any of the optional members and with
403        // other members set to their default values."
404        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
405            let mut statements = vec![new_local(value_type(ast, id))];
406            for member_id in &ast.all_members_of(id) {
407                let Ok(member_node) = ast.member(*member_id) else {
408                    continue;
409                };
410                let field = field_name(&member_node.name);
411                if member_node.facets.is_optional() {
412                    // "Implementations shall not provide a default value
413                    // in such a case" — which here is the null a fresh
414                    // reference or a fresh `Nullable<T>` already holds.
415                    // Written out anyway, so a defaulter run over a
416                    // sample that has been read once before clears it.
417                    statements.push(assign(
418                        member("v", &field),
419                        csharp::Expr::Literal(Literal::Null),
420                    ));
421                    continue;
422                }
423                let Some(member_type) = member_node.type_.resolved() else {
424                    return Err(EncodeError::NoWriter {
425                        member: member_node.name.clone(),
426                        kind: "member with an unresolved type",
427                    });
428                };
429                statements.extend(member_default(
430                    ast,
431                    member_node,
432                    member_type,
433                    member("v", &field),
434                )?);
435            }
436            statements.push(give_back(name("v")));
437            statements
438        }
439
440        // "A union with the discriminator set to the default value for
441        // the discriminator type. If this selects a branch, then the
442        // selected member is also set to the default value for the
443        // member type."
444        TypeKind::Union { discriminator, .. } => {
445            let Some(discriminator) = discriminator.resolved() else {
446                return Err(refuse("union with an unresolved discriminator"));
447            };
448            let mut statements = vec![
449                new_local(value_type(ast, id)),
450                default_into(ast, discriminator, member("v", &field_name("_d"))),
451            ];
452            if let Some((member_type, field)) = selected_by_default(ast, id, discriminator)? {
453                statements.push(default_into(ast, member_type, member("v", &field)));
454            }
455            statements.push(give_back(name("v")));
456            statements
457        }
458
459        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
460            return Ok(None);
461        }
462        TypeKind::ValueBox { .. } => {
463            return Err(refuse(
464                "boxed value, whose presence flag has no settled wire form",
465            ));
466        }
467        // Refused by name rather than passed over. `any` is in the model
468        // because IDL 4.2 has it; DDS-XTypes has no `any` and no
469        // encoding for one, and only CORBA CDR writes it — as a TypeCode
470        // ahead of the value. Returning `Ok(None)` here would generate a
471        // reader that silently omitted the member.
472        TypeKind::Any => {
473            return Err(refuse(
474                "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
475                 as a TypeCode ahead of the value",
476            ));
477        }
478        TypeKind::ValueBase => {
479            return Err(refuse(
480                "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
481                 one, as a chunked value with its repository id",
482            ));
483        }
484    };
485    Ok(Some(statements))
486}
487
488/// Table 9's primitive rows, as the C# holder each takes.
489///
490/// One to one with the IDL width, which is the difference from the Java
491/// column: nothing is widened and nothing is a pattern. The two that are
492/// not a bare `0` are the ones C# will not convert for you — a `float`
493/// needs its suffix, and a `char` is a character.
494fn primitive_default(kind: PrimitiveKind) -> csharp::Expr {
495    match kind {
496        PrimitiveKind::Boolean => csharp::Expr::Literal(Literal::Bool(false)),
497        // `'\0'`, written as the character it is: a bare `0` is an `int`
498        // and C# has no implicit conversion from one to a `char`.
499        PrimitiveKind::Char | PrimitiveKind::WChar => csharp::Expr::Literal(Literal::Char('\0')),
500        // A real literal is a `double`, and there is no implicit
501        // narrowing to `float` even for a constant.
502        PrimitiveKind::Float32 => csharp::Expr::Literal(Literal::Float("0.0f".to_string())),
503        PrimitiveKind::Float64 => csharp::Expr::Literal(Literal::Float("0.0".to_string())),
504        // A binary128 zero is its zero bit pattern, and `Float128.Zero`
505        // names it so the spelling does not depend on the struct layout.
506        PrimitiveKind::Float128 => csharp::Expr::Member {
507            value: Box::new(csharp::Expr::Path(Path::rooted(&[
508                "ZeroDds", "Idl", "Float128",
509            ]))),
510            name: Ident::new("Zero"),
511        },
512        // Every integer width: ECMA-334 §6.4.5.3's implicit constant
513        // expression conversion carries an in-range `int` constant into
514        // every one of them, so the decimal alone lands in the declared
515        // type.
516        PrimitiveKind::Int8
517        | PrimitiveKind::Int16
518        | PrimitiveKind::Int32
519        | PrimitiveKind::Int64
520        | PrimitiveKind::Octet
521        | PrimitiveKind::UInt8
522        | PrimitiveKind::UInt16
523        | PrimitiveKind::UInt32
524        | PrimitiveKind::UInt64 => csharp::Expr::Literal(Literal::int(0)),
525    }
526}
527
528/// `global::<modules>.<Enum>.<LITERAL>` — the enumeration the C#
529/// materialiser declares, written from the global namespace down.
530pub(crate) fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> csharp::Expr {
531    let owner = declared_path(ast, id).unwrap_or_else(|_| {
532        // A path the module chain will not answer for is a name `csc`
533        // reports; a bare constant would compile against whatever else
534        // happened to be in scope.
535        format!("UnresolvedEnum{}", id.slot())
536    });
537    // The enumerator's own name is verbatim but for keyword escaping:
538    // `adapter/csharp.rs` folds a *member* and leaves a type's and an
539    // enumerator's name alone, because a type's name reaches the wire.
540    let name = zerodds_lang_cst::csharp::escape_keyword(enumerator);
541    csharp::Expr::Member {
542        value: Box::new(path(&owner)),
543        name: Ident::new(&name),
544    }
545}
546
547/// The case a defaulted discriminator selects, if any.
548fn selected_by_default(
549    ast: &Ast,
550    id: Id<TypeNode>,
551    discriminator: Id<TypeNode>,
552) -> Result<Option<(Id<TypeNode>, String)>, EncodeError> {
553    let Some(selector) = discriminator_default_value(ast, discriminator) else {
554        return Ok(None);
555    };
556    Ok(selected_case(ast, id, selector)?
557        .map(|(member, member_type)| (member_type, field_name(&member.name))))
558}
559
560/// An explicit `@default` as a C# expression for `type_`.
561#[allow(clippy::too_many_lines)]
562fn explicit(
563    ast: &Ast,
564    expr: Id<zerodds_ast::ConstExprNode>,
565    type_: Id<TypeNode>,
566    member_name: &str,
567) -> Result<csharp::Expr, EncodeError> {
568    let refuse = |kind: &'static str| EncodeError::NoWriter {
569        member: member_name.to_string(),
570        kind,
571    };
572    let node = ast
573        .expr(expr)
574        .map_err(|_| refuse("a `@default` this frontend cannot follow"))?;
575    let resolved = resolve_alias(ast, type_);
576    let kind = ast.type_(resolved).map(|node| node.kind.clone());
577    match &node.expr {
578        ConstExpr::Literal(AstLiteral::Boolean(flag)) => {
579            Ok(csharp::Expr::Literal(Literal::Bool(*flag)))
580        }
581        ConstExpr::Literal(AstLiteral::Char(ch)) => char_literal(u32::from(*ch), refuse),
582        ConstExpr::Literal(AstLiteral::WChar(code)) => char_literal(*code, refuse),
583        ConstExpr::Literal(AstLiteral::String(value)) => Ok(text(value)),
584        ConstExpr::Literal(AstLiteral::WString(codes)) => {
585            let mut out = String::new();
586            for code in codes {
587                let Some(ch) = char::from_u32(*code) else {
588                    return Err(refuse("a `@default` wide string that is not code points"));
589                };
590                out.push(ch);
591            }
592            Ok(text(&out))
593        }
594        ConstExpr::Literal(AstLiteral::Float { mantissa, exponent }) => {
595            let suffix = match kind {
596                Ok(TypeKind::Primitive(PrimitiveKind::Float32)) => "f",
597                _ => "",
598            };
599            let exponent = exponent
600                .as_ref()
601                .map_or(String::new(), |text| format!("e{text}"));
602            Ok(csharp::Expr::Literal(Literal::Float(format!(
603                "{mantissa}{exponent}{suffix}"
604            ))))
605        }
606        // A `fixed<P,S>` is the decimal text here, as the member's type
607        // is a `string`.
608        ConstExpr::Literal(AstLiteral::Fixed { digits, scale }) => {
609            Ok(text(&fixed_text(digits, *scale)))
610        }
611        ConstExpr::Literal(AstLiteral::Enumerator(reference)) => {
612            let target = reference
613                .resolved()
614                .ok_or_else(|| refuse("a `@default` enumerator that is unresolved"))?;
615            let enumerator = ast
616                .enumerator(target)
617                .map_err(|_| refuse("a `@default` enumerator this frontend cannot follow"))?;
618            Ok(enumerator_constant(ast, resolved, &enumerator.name))
619        }
620        _ => {
621            // An integer literal, a named constant or arithmetic over
622            // them, folded the same way a bound or a case label is.
623            let value = ast
624                .eval_integral(expr)
625                .map_err(|_| refuse("a `@default` this frontend cannot evaluate"))?;
626            Ok(match kind {
627                Ok(TypeKind::Primitive(PrimitiveKind::Char | PrimitiveKind::WChar)) => {
628                    // `try_from` used to fold to zero here, so a negative
629                    // `@default` on a character member became NUL with no
630                    // diagnostic — the same loss the surrogate case had,
631                    // reached from the other side of the range.
632                    let code = u32::try_from(value).map_err(|_| {
633                        refuse("a `@default` character outside the range of a code unit")
634                    })?;
635                    char_literal(code, refuse)?
636                }
637                // An enumerated `@default` given as a number: the cast
638                // is what makes it a value of the enumeration, and C#
639                // admits every value of the underlying type.
640                Ok(TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. }) => csharp::Expr::Cast {
641                    type_: Box::new(value_type(ast, resolved)),
642                    value: Box::new(csharp::Expr::Literal(Literal::int(value))),
643                },
644                _ => csharp::Expr::Literal(Literal::int(value)),
645            })
646        }
647    }
648}
649
650/// `(char) <code unit>` — a code unit and not a code point.
651///
652/// A C# `char` is one UTF-16 code unit and holds every value up to
653/// `0xFFFF`, a lone surrogate among them; Rust's `char` holds only
654/// Unicode scalar values and rejects `0xD800..=0xDFFF`. Routing the code
655/// through `char::from_u32` therefore loses exactly the values the two
656/// types disagree about, and this function used to answer that loss with
657/// `'\0'` on the argument that the frontend had already refused the
658/// declaration. It has not: `wireprobe-zerodds-csharp-gen` takes
659/// `@default(0xD800) wchar c` with exit 0 and wrote `v.C = '\0';`, where
660/// the C column wrote `55296` and the Java column `(char) 55296`.
661///
662/// The cast is what the Java backend has always emitted and needs no
663/// Rust `char` at all. Above `0xFFFF` there is no single C# `char`, and
664/// that is refused by name rather than folded.
665fn char_literal(
666    code: u32,
667    refuse: impl Fn(&'static str) -> EncodeError,
668) -> Result<csharp::Expr, EncodeError> {
669    if code > u32::from(u16::MAX) {
670        return Err(refuse(
671            "a `@default` character above the Basic Multilingual Plane, \
672             which no single C# `char` holds",
673        ));
674    }
675    Ok(csharp::Expr::Cast {
676        type_: Box::new(csharp::Type::Predefined(csharp::Predefined::Char)),
677        value: Box::new(csharp::Expr::Literal(Literal::int(i128::from(code)))),
678    })
679}
680
681// -- reading the calls back out ----------------------------------------
682
683fn calls_in_member(member: &csharp::Member, out: &mut Vec<String>) {
684    match member {
685        csharp::Member::Method(method) => {
686            if let Some(body) = &method.body {
687                calls_in_block(body, out);
688            }
689            if let Some(value) = &method.expression {
690                calls_in_expr(value, out);
691            }
692        }
693        csharp::Member::Field(field) => {
694            if let Some(value) = &field.value {
695                calls_in_expr(value, out);
696            }
697        }
698        csharp::Member::Property(property) => {
699            if let Some(value) = &property.expression {
700                calls_in_expr(value, out);
701            }
702            if let Some(value) = &property.initialiser {
703                calls_in_expr(value, out);
704            }
705        }
706        csharp::Member::Constructor(constructor) => calls_in_block(&constructor.body, out),
707        csharp::Member::Nested(_) | csharp::Member::Comment(_) | csharp::Member::Verbatim(_) => {}
708    }
709}
710
711/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
712/// The tree walked here is the one this generator emitted and owns, so
713/// it holds no cycle; the depth is that body's height, and a defaulter
714/// body is a handful of statements deep.
715fn calls_in_block(block: &csharp::Block, out: &mut Vec<String>) {
716    for statement in &block.statements {
717        calls_in_statement(statement, out);
718    }
719}
720
721/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
722/// The tree walked here is the one this generator emitted and owns, so
723/// it holds no cycle; the depth is that body's height, and a defaulter
724/// body is a handful of statements deep.
725fn calls_in_statement(statement: &csharp::Statement, out: &mut Vec<String>) {
726    match statement {
727        csharp::Statement::Declare { value, .. } => {
728            if let Some(value) = value {
729                calls_in_expr(value, out);
730            }
731        }
732        csharp::Statement::Expr(expr) | csharp::Statement::Throw(expr) => {
733            calls_in_expr(expr, out);
734        }
735        csharp::Statement::Return(value) => {
736            if let Some(value) = value {
737                calls_in_expr(value, out);
738            }
739        }
740        csharp::Statement::If {
741            condition,
742            then,
743            otherwise,
744        } => {
745            calls_in_expr(condition, out);
746            calls_in_block(then, out);
747            if let Some(otherwise) = otherwise {
748                calls_in_block(otherwise, out);
749            }
750        }
751        csharp::Statement::ForEach { range, body, .. } => {
752            calls_in_expr(range, out);
753            calls_in_block(body, out);
754        }
755        csharp::Statement::For {
756            init,
757            condition,
758            step,
759            body,
760        } => {
761            calls_in_statement(init, out);
762            calls_in_expr(condition, out);
763            calls_in_expr(step, out);
764            calls_in_block(body, out);
765        }
766        csharp::Statement::While { condition, body } => {
767            calls_in_expr(condition, out);
768            calls_in_block(body, out);
769        }
770        csharp::Statement::Switch { value, sections } => {
771            calls_in_expr(value, out);
772            for section in sections {
773                for label in &section.labels {
774                    calls_in_expr(label, out);
775                }
776                calls_in_block(&section.body, out);
777            }
778        }
779        csharp::Statement::Using { resource, body } => {
780            calls_in_statement(resource, out);
781            calls_in_block(body, out);
782        }
783        csharp::Statement::Block(block) => calls_in_block(block, out),
784        csharp::Statement::Break | csharp::Statement::Comment(_) => {}
785    }
786}
787
788/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
789/// The tree walked here is the one this generator emitted and owns, so
790/// it holds no cycle; the depth is that expression's height, which
791/// follows the member nesting the type walk already bounded.
792#[allow(clippy::too_many_lines)]
793fn calls_in_expr(expr: &csharp::Expr, out: &mut Vec<String>) {
794    match expr {
795        csharp::Expr::Invoke {
796            receiver,
797            name,
798            args,
799        } => {
800            // Unqualified only: a defaulter is a static method of the
801            // generated class and this generator built every call to one
802            // with `defaulter_name`.
803            if receiver.is_none() {
804                let name = name.as_str();
805                if name.starts_with("zd_def_") && !out.iter().any(|seen| seen == name) {
806                    out.push(name.to_string());
807                }
808            }
809            if let Some(receiver) = receiver {
810                calls_in_expr(receiver, out);
811            }
812            for arg in args {
813                calls_in_expr(arg, out);
814            }
815        }
816        csharp::Expr::Member { value, .. }
817        | csharp::Expr::Cast { value, .. }
818        | csharp::Expr::Is { value, .. }
819        | csharp::Expr::Unary { value, .. }
820        | csharp::Expr::NameOf(value)
821        | csharp::Expr::Paren(value) => calls_in_expr(value, out),
822        csharp::Expr::Index { value, index } => {
823            calls_in_expr(value, out);
824            calls_in_expr(index, out);
825        }
826        csharp::Expr::New {
827            args, initialiser, ..
828        } => {
829            for arg in args {
830                calls_in_expr(arg, out);
831            }
832            for (_, value) in initialiser {
833                calls_in_expr(value, out);
834            }
835        }
836        csharp::Expr::NewArray {
837            length, elements, ..
838        } => {
839            if let Some(length) = length {
840                calls_in_expr(length, out);
841            }
842            for element in elements {
843                calls_in_expr(element, out);
844            }
845        }
846        csharp::Expr::Binary { lhs, rhs, .. } => {
847            calls_in_expr(lhs, out);
848            calls_in_expr(rhs, out);
849        }
850        csharp::Expr::Assign { target, value } => {
851            calls_in_expr(target, out);
852            calls_in_expr(value, out);
853        }
854        csharp::Expr::Conditional {
855            condition,
856            then,
857            otherwise,
858        } => {
859            calls_in_expr(condition, out);
860            calls_in_expr(then, out);
861            calls_in_expr(otherwise, out);
862        }
863        csharp::Expr::Lambda { body, .. } => calls_in_expr(body, out),
864        csharp::Expr::Literal(_)
865        | csharp::Expr::Path(_)
866        | csharp::Expr::This
867        | csharp::Expr::Default(_)
868        | csharp::Expr::TypeOf(_) => {}
869    }
870}
871
872// -- the small helpers --------------------------------------------------
873
874fn path(text: &str) -> csharp::Expr {
875    csharp::Expr::Path(match text.strip_prefix("global::") {
876        Some(rest) => Path::rooted(&rest.split('.').collect::<Vec<_>>()),
877        None => Path::new(&text.split('.').collect::<Vec<_>>()),
878    })
879}
880
881fn name(text: &str) -> csharp::Expr {
882    path(text)
883}
884
885fn text(value: &str) -> csharp::Expr {
886    csharp::Expr::Literal(Literal::Str(value.to_string()))
887}
888
889fn call(callee: &str, args: Vec<csharp::Expr>) -> csharp::Expr {
890    csharp::Expr::Invoke {
891        receiver: None,
892        name: Ident::new(callee),
893        args,
894    }
895}
896
897fn member(base: &str, field: &str) -> csharp::Expr {
898    csharp::Expr::Member {
899        value: Box::new(name(base)),
900        name: Ident::new(field),
901    }
902}
903
904fn assign(place: csharp::Expr, value: csharp::Expr) -> csharp::Statement {
905    csharp::Statement::Expr(csharp::Expr::Binary {
906        op: "=",
907        lhs: Box::new(place),
908        rhs: Box::new(value),
909    })
910}
911
912fn give_back(value: csharp::Expr) -> csharp::Statement {
913    csharp::Statement::Return(Some(value))
914}
915
916/// `<T> v = new <T>();`
917fn new_local(type_: csharp::Type) -> csharp::Statement {
918    csharp::Statement::Declare {
919        type_: type_.clone(),
920        name: Ident::new("v"),
921        value: Some(csharp::Expr::New {
922            type_: Box::new(type_),
923            args: Vec::new(),
924            initialiser: Vec::new(),
925        }),
926    }
927}
928
929fn counted_loop(index: &str, count: csharp::Expr, body: csharp::Block) -> csharp::Statement {
930    csharp::Statement::For {
931        init: Box::new(csharp::Statement::Declare {
932            type_: csharp::Type::Predefined(csharp::Predefined::Int),
933            name: Ident::new(index),
934            value: Some(csharp::Expr::Literal(Literal::int(0))),
935        }),
936        condition: csharp::Expr::Binary {
937            op: "<",
938            lhs: Box::new(name(index)),
939            rhs: Box::new(count),
940        },
941        step: csharp::Expr::Binary {
942            op: "=",
943            lhs: Box::new(name(index)),
944            rhs: Box::new(csharp::Expr::Binary {
945                op: "+",
946                lhs: Box::new(name(index)),
947                rhs: Box::new(csharp::Expr::Literal(Literal::int(1))),
948            }),
949        },
950        body,
951    }
952}
953
954/// Kept so the module's one dependency on the materialiser's error type
955/// is stated rather than inferred.
956const _: Option<MaterialiseError> = None;