Skip to main content

zerodds_lang_rules/emit/
d.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The D backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as the seven columns
6//! before it, and none of them calls another. Where two columns
7//! disagree on an octet, one has misread the specification — which is
8//! the only reason to have a ninth.
9//!
10//! # The four decisions this column turns on
11//!
12//! **A member holds its wire width, under D's own names.** An IDL
13//! `long` is a D `int` and an IDL `long long` a D `long`; the adapter's
14//! table carries the warning at length. A `char` is `ubyte` and a
15//! `wchar` `ushort`, so [`CodecTarget::code_point`] is the identity and
16//! a union's discriminator compares as a number.
17//!
18//! **`@optional` is `Nullable!(T)` and `@external` is `T*`, and the
19//! four seams keep them apart.** `!v.m.isNull` against `v.m !is null`,
20//! `v.m.get` against `*v.m`, `zdwire.zdNullable(..)` against
21//! `zdwire.zdPtr(..)` — and `@optional @external` takes the pointer
22//! forms, because a `Nullable` over a pointer would carry a
23//! present-but-null state the wire form has no encoding for.
24//!
25//! **A `long double` and a `fixed` are decimal text.** D's `real` is
26//! whatever the target has — 80-bit x87 on x86, plain binary64 on
27//! AArch64 — and a binary128 has 113 significand bits, so `runtime/d`
28//! parses and prints the member's decimal exactly.
29//!
30//! **Every runtime name is qualified.** `zdwire.Writer`,
31//! `zdwire.TryConstructDiscard`, `zdwire.zdPtr`: a generated
32//! declaration and a runtime name share one module scope in D, and a
33//! corpus case declaring `struct Writer` would otherwise take the name
34//! over.
35
36use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
37use crate::adapter::d::{d_map_entry_name, d_type_name, d_value_type, field_name};
38use crate::encode::EncodeError;
39use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
40use zerodds_ast_core::Id;
41use zerodds_lang_cst::common::{Doc, Ident, Literal};
42use zerodds_lang_cst::d;
43
44/// The D backend.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct D;
47
48/// How deep a chain of typedefs and collections is followed.
49const MAX_TYPE_DEPTH: usize = 64;
50
51/// The module the generated file imports the runtime under.
52const RUNTIME: &str = "zdwire";
53
54/// The type name [`CodecTarget::named`] is handed for C's signed
55/// `char`; the D member is a `ubyte` and the signed reading is `byte`.
56const SIGNED_CHAR: &str = "signed char";
57
58/// An expression, with the one property the walk's seams cannot carry:
59/// which indirection the place it names lives behind.
60#[derive(Debug, Clone)]
61pub enum DExpr {
62    /// An ordinary expression.
63    Plain(d::Expr),
64    /// The `Nullable!(T)` field of an `@optional` member, as a place a
65    /// decoded `T` is about to be written into.
66    OptionalSlot(d::Expr),
67    /// The `T*` field of an `@external` member, likewise.
68    ExternalSlot(d::Expr),
69}
70
71/// The expression inside, whichever form it took.
72fn inner(expr: DExpr) -> d::Expr {
73    match expr {
74        DExpr::Plain(e) | DExpr::OptionalSlot(e) | DExpr::ExternalSlot(e) => e,
75    }
76}
77
78fn plain(e: d::Expr) -> DExpr {
79    DExpr::Plain(e)
80}
81
82impl D {
83    /// A runtime name, qualified: `zdwire.X`.
84    fn runtime_name(name: &str) -> d::Expr {
85        d::Expr::Dot {
86            value: Box::new(d::Expr::Name(Ident::new(RUNTIME))),
87            name: Ident::new(name),
88        }
89    }
90
91    /// A runtime operation's D spelling: `write_u32` is `WriteU32`,
92    /// each `_`-separated segment with its first letter raised — the
93    /// names `runtime/d` publishes, because that runtime was written
94    /// against this conversion.
95    fn pascal(method: &str) -> String {
96        let mut out = String::new();
97        for segment in method.split('_') {
98            let mut chars = segment.chars();
99            if let Some(first) = chars.next() {
100                out.extend(first.to_uppercase());
101                out.push_str(chars.as_str());
102            }
103        }
104        out
105    }
106
107    fn text(value: &str) -> DExpr {
108        plain(d::Expr::Literal(Literal::Str(value.to_string())))
109    }
110
111    fn runtime_constant(name: &str) -> DExpr {
112        plain(Self::runtime_name(name))
113    }
114
115    fn dot(value: d::Expr, name: &str) -> d::Expr {
116        d::Expr::Dot {
117            value: Box::new(value),
118            name: Ident::new(name),
119        }
120    }
121
122    fn call_expr(func: d::Expr, args: Vec<d::Expr>) -> d::Expr {
123        d::Expr::Call {
124            func: Box::new(func),
125            args,
126        }
127    }
128}
129
130impl CodecTarget for D {
131    type Expr = DExpr;
132    type Stmt = d::Stmt;
133    type Block = Vec<d::Stmt>;
134    type Type = d::Type;
135    type Decl = d::Decl;
136
137    // -- types --------------------------------------------------------
138
139    /// The fixed-width D type each scalar local takes. `Index` is
140    /// `uint` — a sequence length is a `uint32` on the wire — and
141    /// `Size` is `int`, which is what the reader's positions answer in.
142    fn scalar(kind: Scalar) -> Self::Type {
143        match kind {
144            Scalar::Bool => d::Type::Primitive(d::Primitive::Bool),
145            Scalar::Char | Scalar::UInt8 => d::Type::Primitive(d::Primitive::UByte),
146            Scalar::Int8 => d::Type::Primitive(d::Primitive::Byte),
147            Scalar::Int16 => d::Type::Primitive(d::Primitive::Short),
148            Scalar::UInt16 => d::Type::Primitive(d::Primitive::UShort),
149            Scalar::Int32 | Scalar::Size => d::Type::Primitive(d::Primitive::Int),
150            Scalar::UInt32 | Scalar::Index => d::Type::Primitive(d::Primitive::UInt),
151            Scalar::Int64 => d::Type::Primitive(d::Primitive::Long),
152            Scalar::UInt64 => d::Type::Primitive(d::Primitive::ULong),
153            Scalar::Float => d::Type::Primitive(d::Primitive::Float),
154            Scalar::Double => d::Type::Primitive(d::Primitive::Double),
155            // The decimal text; the module documentation says why.
156            Scalar::LongDouble => d::Type::Primitive(d::Primitive::String),
157        }
158    }
159
160    fn named(name: &str) -> Self::Type {
161        d::Type::named(name)
162    }
163
164    /// `void`, which is what a D function that answers nothing returns.
165    fn nothing() -> Self::Type {
166        d::Type::Primitive(d::Primitive::Void)
167    }
168
169    /// The runtime's handles, qualified.
170    fn handle(kind: Handle) -> Self::Type {
171        d::Type::named(match kind {
172            Handle::DHeader => "zdwire.DHeader",
173            Handle::EmHeader => "zdwire.EmHeader",
174            Handle::Pid => "zdwire.Pid",
175            Handle::TryConstruct => "zdwire.TryConstruct",
176            Handle::ReaderMark => "zdwire.ReaderMark",
177            Handle::PidRead => "zdwire.PidRead",
178            Handle::EmHeaderRead => "zdwire.EmHeaderRead",
179        })
180    }
181
182    /// The member already is the code unit — `ubyte` for a `char`,
183    /// `ushort` for a `wchar` — so there is nothing to convert.
184    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
185        value
186    }
187
188    /// The runtime's policy values: strings and integers where
189    /// `runtime/d` takes them so, and qualified constants for the
190    /// enumerated kinds. One arm per constant and no catch-all.
191    fn constant(which: Constant) -> Self::Expr {
192        match which {
193            Constant::WcharWidth2 => plain(d::Expr::int(2)),
194            Constant::WcharWidth4 => plain(d::Expr::int(4)),
195            Constant::WcharFixed => Self::text("fixed"),
196            Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
197            Constant::WstringUnitsStream => Self::text("stream"),
198            Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
199            Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
200            Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
201            Constant::WstringSpecification => Self::text("specification"),
202            Constant::WstringCharacterCount => Self::text("character-count"),
203            Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
204            Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
205            Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
206            Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
207            Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
208            Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
209            Constant::FixedDeclared => Self::text("declared"),
210            Constant::FixedValuePrecision => Self::text("value-precision"),
211            Constant::FixedValueStringDigits => Self::text("value-string-digits"),
212            Constant::ParameterMember => Self::runtime_constant("ParameterMember"),
213            Constant::ParameterOptional => Self::runtime_constant("ParameterOptional"),
214            Constant::TryConstructDiscard => Self::runtime_constant("TryConstructDiscard"),
215            Constant::TryConstructTrim => Self::runtime_constant("TryConstructTrim"),
216            Constant::TryConstructUseDefault => Self::runtime_constant("TryConstructUseDefault"),
217            Constant::StatusOk => Self::runtime_constant("StatusOk"),
218            Constant::StatusDiscarded => Self::runtime_constant("StatusDiscarded"),
219            Constant::StatusImpossibleLength => Self::runtime_constant("StatusImpossibleLength"),
220            Constant::StatusOutOfRange => Self::runtime_constant("StatusOutOfRange"),
221            Constant::StatusTruncated => Self::runtime_constant("StatusTruncated"),
222        }
223    }
224
225    /// The unsigned type wide enough for `bits` of a bit set — the same
226    /// answer the materialiser's `d_holder` gives its fields.
227    fn unsigned_holder(bits: u16) -> Self::Type {
228        d::Type::Primitive(crate::adapter::d::d_holder(bits))
229    }
230
231    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
232        // Written as the match `unwrap_or` folds away, so the fallback
233        // carries its reason where the silent-drop guard reads it: a
234        // type with no D form was refused by the materialiser before a
235        // codec walk ran, and `void` makes the impossible fail in the D
236        // compiler, loudly, rather than compile into a plausible
237        // member.
238        #[allow(clippy::manual_unwrap_or_default, clippy::manual_unwrap_or)]
239        match d_value_type(ast, id) {
240            Ok(type_) => type_,
241            Err(_) => d::Type::Primitive(d::Primitive::Void),
242        }
243    }
244
245    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
246        type_tag(ast, id, 0)
247    }
248
249    /// The entry name the materialiser declared, and a distinct one
250    /// per type when its tag walk refuses: the slot keeps two refused
251    /// entries from collapsing onto one declaration, which would merge
252    /// two map types under one name.
253    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
254        d_map_entry_name(ast, id).unwrap_or_else(|_| format!("ZdUnresolvedEntry{}", id.slot()))
255    }
256
257    /// Nothing: a declared type's D name is already flat —
258    /// `d_type_name` folds the modules in — and an enumerator lives in
259    /// its enum's own scope, which
260    /// [`CodecTarget::enumerator_constant`]'s override reaches.
261    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
262        String::new()
263    }
264
265    /// The escape and nothing else, exactly as the materialiser spells
266    /// a field.
267    fn escape(name: &str) -> String {
268        field_name(name)
269    }
270
271    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
272    fn inherited_path(depth: usize) -> Option<String> {
273        Some(format!("{}.", field_name("parent")).repeat(depth))
274    }
275
276    // -- expressions ---------------------------------------------------
277
278    fn name(text: &str) -> Self::Expr {
279        plain(d::Expr::Name(Ident::new(text)))
280    }
281
282    fn int(value: i128) -> Self::Expr {
283        plain(d::Expr::int(value))
284    }
285
286    /// The raw tokens the walk escapes through [`Literal::Int`],
287    /// rewritten into the tree's own boolean where they are one.
288    fn literal(value: Literal) -> Self::Expr {
289        let Literal::Int(text) = &value else {
290            return plain(d::Expr::Literal(value));
291        };
292        match text.as_str() {
293            "true" => plain(d::Expr::Literal(Literal::Bool(true))),
294            "false" => plain(d::Expr::Literal(Literal::Bool(false))),
295            _ => plain(d::Expr::Literal(value)),
296        }
297    }
298
299    /// A literal wide enough to survive its own width: D types an
300    /// unsuffixed integer literal `int`, so a union label past `int`
301    /// carries `L` and one past `long` carries `UL`.
302    ///
303    /// −2^63 is the one value the `L` cannot be written on. D's lexical
304    /// grammar gives an integer literal no sign ("Lexical", Integer
305    /// Literals), so `-9223372036854775808L` is a minus over a literal
306    /// whose `L` asks for a `long` it does not fit, and both compilers
307    /// this column is measured with refuse it outright rather than
308    /// warn: ldc2 1.40.0 and gdc 14.2.0 both say
309    /// `Error: signed integer overflow`. `long.min` is the value under
310    /// its own name — the `.min` property D defines for every integral
311    /// type ("Properties for Integral Data Types") — and it is a
312    /// compile-time constant, so it stands wherever the suffixed
313    /// literal did: inside the `cast(long)` of a union comparison, and
314    /// as the initialiser of an `enum` manifest constant. The same
315    /// answer `emit/kotlin.rs` gives with `Long.MIN_VALUE`, for the
316    /// same reason.
317    fn wide_int(value: i128) -> Self::Expr {
318        if value > i128::from(i64::MAX) {
319            return plain(d::Expr::suffixed_int(value, "UL"));
320        }
321        if value == i128::from(i64::MIN) {
322            return plain(d::Expr::Literal(Literal::Int("long.min".to_string())));
323        }
324        if value > i128::from(i32::MAX) || value < i128::from(i32::MIN) {
325            return plain(d::Expr::suffixed_int(value, "L"));
326        }
327        plain(d::Expr::int(value))
328    }
329
330    /// The number, with no `u` suffix: an `int` literal converts
331    /// against a `uint` counter under D's value range propagation, and
332    /// a bound past `int` gets its width from [`CodecTarget::wide_int`].
333    fn unsigned_int(value: i128) -> Self::Expr {
334        Self::wide_int(value)
335    }
336
337    /// `..UL`, typed: a mask the walk complements has to already be a
338    /// `ulong`, and `~` on an `int` would sign-extend a different
339    /// pattern.
340    fn wide_unsigned(value: u64) -> Self::Expr {
341        plain(d::Expr::suffixed_int(i128::from(value), "UL"))
342    }
343
344    /// `..L`, for the enumerator patterns `zd_raw` is compared against.
345    fn wide_signed(value: i128) -> Self::Expr {
346        plain(d::Expr::suffixed_int(value, "L"))
347    }
348
349    /// `v.x` — and `v._d` as it stands: D keeps the IDL-facing
350    /// spelling, underscore and all.
351    fn member(base: &str, field: &str) -> Self::Expr {
352        plain(Self::dot(d::Expr::Name(Ident::new(base)), field))
353    }
354
355    fn whole(base: &str) -> Self::Expr {
356        plain(d::Expr::Name(Ident::new(base)))
357    }
358
359    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
360        plain(Self::dot(inner(value), field))
361    }
362
363    /// A header's field, in the spelling `runtime/d` publishes:
364    /// `member_id` is `MemberId`.
365    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
366        plain(Self::dot(inner(value), &Self::pascal(field)))
367    }
368
369    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
370        plain(d::Expr::Index {
371            value: Box::new(inner(value)),
372            index: Box::new(inner(at)),
373        })
374    }
375
376    /// `cast(uint)v.length`, or the bound where the materialiser fixed
377    /// one: `.length` is a `size_t` and a sequence's count is a
378    /// `uint32` on the wire.
379    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
380        match fixed {
381            Some(bound) => plain(d::Expr::int(i128::from(bound))),
382            None => plain(d::Expr::Cast {
383                type_: Box::new(d::Type::Primitive(d::Primitive::UInt)),
384                value: Box::new(Self::dot(d::Expr::Name(Ident::new(base)), "length")),
385            }),
386        }
387    }
388
389    /// The slice itself: a D slice is the elements.
390    fn elements(base: &str) -> Self::Expr {
391        plain(d::Expr::Name(Ident::new(base)))
392    }
393
394    /// `v.x` — the anonymous union puts its fields directly on the
395    /// enclosing struct.
396    fn case_member(base: &str, field: &str) -> Self::Expr {
397        Self::member(base, field)
398    }
399
400    /// `entry.key` and `entry.value`, the two fields the materialiser
401    /// gives a map's entry struct.
402    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
403        plain(Self::dot(inner(entry), if key { "key" } else { "value" }))
404    }
405
406    fn absent() -> Self::Expr {
407        plain(d::Expr::Literal(Literal::Null))
408    }
409
410    /// Nothing: a D string and a D slice own their storage, and there
411    /// is no allocation failure for the guard to test.
412    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
413        None
414    }
415
416    /// `v.x.get` — the value a `Nullable` holds, given that it holds
417    /// one. Only ever reached inside a branch the presence test
418    /// guarded, where `.get`'s own assertion cannot fire.
419    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
420        plain(Self::dot(inner(Self::member(base, field)), "get"))
421    }
422
423    /// `!v.x.isNull`.
424    fn present(base: &str, field: &str) -> Self::Expr {
425        plain(d::Expr::Unary {
426            op: "!",
427            value: Box::new(Self::dot(inner(Self::member(base, field)), "isNull")),
428        })
429    }
430
431    /// Nothing on the way in — the assignment through
432    /// `zdwire.zdNullable` is what makes the member present — and
433    /// `v.x.nullify();` on the way out.
434    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
435        if present {
436            return Vec::new();
437        }
438        vec![d::Stmt::Expr(Self::call_expr(
439            Self::dot(inner(Self::member(base, field)), "nullify"),
440            Vec::new(),
441        ))]
442    }
443
444    /// The marked place; the room is made by [`D::read_call`], which
445    /// routes the value through `zdwire.zdNullable`.
446    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
447        (
448            Vec::new(),
449            DExpr::OptionalSlot(inner(Self::member(base, field))),
450        )
451    }
452
453    /// `true`: an `@external` member is `T*`, and a member on a cycle needs the
454    /// same: `Nullable!(T)` stores its `T` by value.
455    /// [`crate::adapter::held_externally`] is the rule the adapter and
456    /// the defaulter ask as well.
457    fn holds_a_cycle_externally() -> bool {
458        true
459    }
460
461    /// `*v.x` — the pointer dereferenced, for both `@external` and
462    /// `@optional @external`: the member's codec takes the value.
463    fn external_value(
464        _ast: &Ast,
465        _id: Id<TypeNode>,
466        base: &str,
467        field: &str,
468        _optional: bool,
469    ) -> Self::Expr {
470        plain(d::Expr::Unary {
471            op: "*",
472            value: Box::new(inner(Self::member(base, field))),
473        })
474    }
475
476    /// The marked place; [`D::read_call`] routes the decoded value
477    /// through `zdwire.zdPtr`.
478    fn external_destination(
479        base: &str,
480        field: &str,
481        _optional: bool,
482    ) -> (Vec<Self::Stmt>, Self::Expr) {
483        (
484            Vec::new(),
485            DExpr::ExternalSlot(inner(Self::member(base, field))),
486        )
487    }
488
489    /// `*v.x` for a branch too: the union arm's field is the same `T*`
490    /// a structure's `@external` member is, and the branch's codec takes
491    /// the value.
492    fn external_case_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
493        plain(d::Expr::Unary {
494            op: "*",
495            value: Box::new(inner(Self::case_member(base, field))),
496        })
497    }
498
499    /// The marked place, exactly as [`D::external_destination`] answers
500    /// for a structure member; [`D::read_call`] routes the decoded value
501    /// through `zdwire.zdPtr`.
502    fn external_case_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
503        (
504            Vec::new(),
505            DExpr::ExternalSlot(inner(Self::case_member(base, field))),
506        )
507    }
508
509    /// `v.x !is null`: the pointer is the absence, with no `Nullable`
510    /// around it to carry a second one.
511    fn external_present(base: &str, field: &str) -> Self::Expr {
512        plain(d::Expr::Binary {
513            op: "!is",
514            lhs: Box::new(inner(Self::member(base, field))),
515            rhs: Box::new(d::Expr::Literal(Literal::Null)),
516        })
517    }
518
519    /// Nothing on the way in, `v.x = null;` on the way out.
520    fn external_set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
521        if present {
522            return Vec::new();
523        }
524        vec![d::Stmt::Assign {
525            place: inner(Self::member(base, field)),
526            value: d::Expr::Literal(Literal::Null),
527        }]
528    }
529
530    /// The operators as given: D spells `&&`, `||`, `~` and the
531    /// comparisons the way the walk does. `=` never reaches a rendered
532    /// expression — [`D::perform`] turns it into an assignment
533    /// statement.
534    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
535        plain(d::Expr::Binary {
536            op,
537            lhs: Box::new(inner(lhs)),
538            rhs: Box::new(inner(rhs)),
539        })
540    }
541
542    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
543        plain(d::Expr::Unary {
544            op,
545            value: Box::new(inner(value)),
546        })
547    }
548
549    fn paren(value: Self::Expr) -> Self::Expr {
550        plain(d::Expr::Paren(Box::new(inner(value))))
551    }
552
553    /// `cast(T)v`, with two rewrites: a `bool` discriminator's integer
554    /// label becomes the boolean literal, and the walk's `signed char`
555    /// — the C spelling of a map key's signed order — is `byte` here,
556    /// which reads a `ubyte` key as the two's complement the corpus was
557    /// recorded under.
558    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
559        if type_ == d::Type::named(SIGNED_CHAR) {
560            return plain(d::Expr::Cast {
561                type_: Box::new(d::Type::Primitive(d::Primitive::Byte)),
562                value: Box::new(inner(value)),
563            });
564        }
565        if type_ == d::Type::Primitive(d::Primitive::Bool) {
566            if let DExpr::Plain(d::Expr::Literal(Literal::Int(text))) = &value {
567                return plain(d::Expr::Literal(Literal::Bool(text != "0")));
568            }
569        }
570        plain(d::Expr::Cast {
571            type_: Box::new(type_),
572            value: Box::new(inner(value)),
573        })
574    }
575
576    /// The value, unchanged: D hands a slice by header, a static array
577    /// by value and a struct by value.
578    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
579        value
580    }
581
582    /// A call to a package-level function — a generated codec or
583    /// defaulter. `strcmp` is the runtime's `Strcmp`, which spells C11
584    /// §7.24.4's unsigned-octet order in a language whose own string
585    /// order over UTF-8 is exactly that.
586    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
587        let arguments: Vec<d::Expr> = args.into_iter().map(inner).collect();
588        if callee == "strcmp" {
589            return plain(Self::call_expr(Self::runtime_name("Strcmp"), arguments));
590        }
591        plain(Self::call_expr(
592            d::Expr::Name(Ident::new(callee)),
593            arguments,
594        ))
595    }
596
597    /// `w.WriteU32(x)` — a method on the writer or the reader. One
598    /// operation is renamed rather than converted: the walk's
599    /// `read_pid()` opens a value block §7.4.1.2 counts alignment from,
600    /// which is `ReadPidAs` in this runtime; the Python and Go columns
601    /// do the same.
602    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
603        if method == "read_pid" && args.is_empty() {
604            return Self::runtime(
605                receiver,
606                "read_pid_as",
607                vec![Self::constant(Constant::ParameterMember)],
608            );
609        }
610        plain(Self::call_expr(
611            Self::dot(d::Expr::Name(Ident::new(receiver)), &Self::pascal(method)),
612            args.into_iter().map(inner).collect(),
613        ))
614    }
615
616    /// A free function of the runtime, qualified — except
617    /// `pid_is_user_member`, which is a method on the header the reader
618    /// handed back.
619    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
620        if method == "pid_is_user_member" {
621            let mut arguments = args.into_iter().map(inner);
622            if let Some(header) = arguments.next() {
623                return plain(Self::call_expr(
624                    Self::dot(header, "IsUserMember"),
625                    Vec::new(),
626                ));
627            }
628            // The walk always hands the header; a call without one is a
629            // walk that changed under this file, and a method on a name
630            // no scope declares is the loudest thing D can say here.
631            return plain(Self::call_expr(
632                Self::runtime_name("IsUserMember"),
633                Vec::new(),
634            ));
635        }
636        plain(Self::call_expr(
637            Self::runtime_name(&Self::pascal(method)),
638            args.into_iter().map(inner).collect(),
639        ))
640    }
641
642    /// `runtime/d` carries the three HDDS axes: `BeginPackedElement` /
643    /// `FinishPackedElement` on the writer and `ReaderBeginPackedElement`
644    /// / `ReaderFinishPackedElement` on the reader, the DHEADER pair for
645    /// a sequence element's own length, and `WriteWstringUtf8Octets` /
646    /// `WriteWstringUtf8OctetsBounded` / `ReadWstringUtf8Octets`.
647    fn carries_hdds_axes() -> bool {
648        true
649    }
650
651    // -- statements ----------------------------------------------------
652
653    /// `T name = value;` — the type written, so a reader of the
654    /// generated file sees the width, and definite whether or not the
655    /// value is there.
656    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
657        d::Stmt::Var {
658            name: Ident::new(name),
659            type_: Some(type_),
660            value: value.map(inner),
661        }
662    }
663
664    /// An expression evaluated for its effect — or, where the walk
665    /// built an assignment, the assignment as its own statement, so the
666    /// generated file reads as every other column's does.
667    fn perform(value: Self::Expr) -> Self::Stmt {
668        if let DExpr::Plain(d::Expr::Binary { op: "=", lhs, rhs }) = value {
669            return d::Stmt::Assign {
670                place: *lhs,
671                value: *rhs,
672            };
673        }
674        d::Stmt::Expr(inner(value))
675    }
676
677    /// `return v;`: a D decoder answers with the sample it built.
678    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
679        d::Stmt::Return(Some(match value {
680            Some(value) => inner(value),
681            None => d::Expr::Name(Ident::new("v")),
682        }))
683    }
684
685    fn when(
686        condition: Self::Expr,
687        then: Self::Block,
688        otherwise: Option<Self::Block>,
689    ) -> Self::Stmt {
690        d::Stmt::If {
691            condition: inner(condition),
692            then,
693            otherwise: otherwise.filter(|body| !body.is_empty()),
694        }
695    }
696
697    /// `for (uint i = 0; i < bound; i++)`: every bound the walk hands
698    /// over is a `uint` count off the wire or an `int` literal D's
699    /// value range propagation converts.
700    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
701        d::Stmt::For {
702            counter: Ident::new(counter),
703            type_: Box::new(d::Type::Primitive(d::Primitive::UInt)),
704            bound: Box::new(inner(bound)),
705            body,
706        }
707    }
708
709    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
710        d::Stmt::While {
711            condition: inner(condition),
712            body,
713        }
714    }
715
716    fn leave() -> Self::Stmt {
717        d::Stmt::Break
718    }
719
720    fn note(doc: Doc) -> Self::Stmt {
721        d::Stmt::Comment(doc)
722    }
723
724    /// `cast(void)expr;` — D's spelling of a value read for its effect,
725    /// the same one C writes.
726    fn drop_value(value: Self::Expr) -> Self::Stmt {
727        d::Stmt::Expr(d::Expr::Cast {
728            type_: Box::new(d::Type::Primitive(d::Primitive::Void)),
729            value: Box::new(inner(value)),
730        })
731    }
732
733    /// `cast(void)name;` — a parameter deliberately not read, valid
734    /// whether or not the body goes on to use it.
735    fn mark_used(name: &str) -> Vec<Self::Stmt> {
736        vec![d::Stmt::Expr(d::Expr::Cast {
737            type_: Box::new(d::Type::Primitive(d::Primitive::Void)),
738            value: Box::new(d::Expr::Name(Ident::new(name))),
739        })]
740    }
741
742    /// `place = zd_dec_T(r, behaviour);` — and through
743    /// `zdwire.zdNullable` or `zdwire.zdPtr` where the place is an
744    /// `@optional` or an `@external` member's field: the decoder
745    /// answers a `T` and the field holds a wrapper or a pointer.
746    fn read_call(
747        callee: &str,
748        place: Self::Expr,
749        _form: Handoff,
750        behaviour: Self::Expr,
751    ) -> Self::Stmt {
752        let decoded = Self::call_expr(
753            d::Expr::Name(Ident::new(callee)),
754            vec![d::Expr::Name(Ident::new("r")), inner(behaviour)],
755        );
756        match place {
757            DExpr::OptionalSlot(field) => d::Stmt::Assign {
758                place: field,
759                value: Self::call_expr(Self::runtime_name("zdNullable"), vec![decoded]),
760            },
761            DExpr::ExternalSlot(field) => d::Stmt::Assign {
762                place: field,
763                value: Self::call_expr(Self::runtime_name("zdPtr"), vec![decoded]),
764            },
765            DExpr::Plain(place) => d::Stmt::Assign {
766                place,
767                value: decoded,
768            },
769        }
770    }
771
772    /// `v = zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
773    fn read_forward(callee: &str) -> Self::Stmt {
774        Self::read_call(
775            callee,
776            Self::whole("v"),
777            Handoff::Value,
778            Self::name("zd_tc"),
779        )
780    }
781
782    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
783        statements
784    }
785
786    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
787        block
788    }
789
790    /// `v = new T[](n);`, and the fill after it: a D slice's slots are
791    /// storage the moment `new` returns, for a sequence's elements and
792    /// a map entry's halves alike, so `assigned_whole` makes no
793    /// difference here.
794    fn make_room(
795        base: &str,
796        element: Self::Type,
797        count: Self::Expr,
798        _assigned_whole: bool,
799        fill: Vec<Self::Stmt>,
800    ) -> Vec<Self::Stmt> {
801        let mut out = vec![d::Stmt::Assign {
802            place: d::Expr::Name(Ident::new(base)),
803            value: d::Expr::NewSlice {
804                element: Box::new(element),
805                length: Box::new(inner(count)),
806            },
807        }];
808        out.extend(fill);
809        out
810    }
811
812    /// `v = v[0 .. count];` — §7.3.1.2.1.12.3's TRIM, applied after
813    /// every element has been read.
814    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
815        vec![d::Stmt::Assign {
816            place: d::Expr::Name(Ident::new(base)),
817            value: d::Expr::SliceTo {
818                value: Box::new(d::Expr::Name(Ident::new(base))),
819                high: Box::new(inner(count)),
820            },
821        }]
822    }
823
824    /// The subscript, with nothing around it: a D slice element is a
825    /// real place at every element type.
826    fn sequence_element(
827        base: &str,
828        index: Self::Expr,
829        _element: Self::Type,
830    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
831        (
832            Vec::new(),
833            Self::index(Self::elements(base), index),
834            Vec::new(),
835        )
836    }
837
838    // -- assembly ------------------------------------------------------
839
840    /// `void zd_enc_T(zdwire.Writer w, T v)` and
841    /// `T zd_dec_T(zdwire.Reader r, zdwire.TryConstruct zd_tc)`.
842    ///
843    /// A forward declaration is answered with nothing: a D module-scope
844    /// name is in scope for the whole module however far down it is
845    /// written, so a recursive type needs no declaration ahead of its
846    /// body.
847    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
848        let body = spec.body?;
849        let mut params = vec![d::Param {
850            name: Ident::new(if spec.writing { "w" } else { "r" }),
851            type_: d::Type::named(if spec.writing {
852                "zdwire.Writer"
853            } else {
854                "zdwire.Reader"
855            }),
856        }];
857        if spec.writing {
858            params.push(d::Param {
859                name: Ident::new("v"),
860                type_: spec.sample.clone(),
861            });
862        }
863        for (type_, name) in spec.extra {
864            params.push(d::Param {
865                name: Ident::new(&name),
866                type_,
867            });
868        }
869        let (returns, body) = if spec.writing {
870            (None, body)
871        } else {
872            // The sample starts as Table 9's value, through the
873            // generated defaulter, so every position the stream does
874            // not carry is already what the clause says it is.
875            let defaulter = crate::default_d::defaulter_from_decoder(&spec.name);
876            let mut statements = vec![d::Stmt::Var {
877                name: Ident::new("v"),
878                type_: None,
879                value: Some(Self::call_expr(
880                    d::Expr::Name(Ident::new(&defaulter)),
881                    Vec::new(),
882                )),
883            }];
884            statements.extend(body);
885            statements.push(d::Stmt::Return(Some(d::Expr::Name(Ident::new("v")))));
886            (Some(spec.sample), statements)
887        };
888        Some(d::Decl::Func(d::FuncDecl {
889            doc: spec.doc,
890            name: Ident::new(&spec.name),
891            params,
892            returns,
893            body,
894        }))
895    }
896
897    /// A call to the generated defaulter, which is Table 9 once per
898    /// type rather than once per position.
899    ///
900    /// # Errors
901    /// Never; the signature matches the emitters around it.
902    fn default_into(
903        ast: &Ast,
904        id: Id<TypeNode>,
905        place: Self::Expr,
906    ) -> Result<Self::Stmt, EncodeError> {
907        let defaulted = Self::call_expr(
908            d::Expr::Name(Ident::new(&crate::default_d::defaulter_name(ast, id))),
909            Vec::new(),
910        );
911        // The slot markers route exactly as [`D::read_call`]'s do: the
912        // defaulter answers a `T` and an `@external` field holds a `T*`,
913        // an `@optional` one a `Nullable!(T)`. Assigning the bare value
914        // into either is not D.
915        Ok(match place {
916            DExpr::OptionalSlot(field) => d::Stmt::Assign {
917                place: field,
918                value: Self::call_expr(Self::runtime_name("zdNullable"), vec![defaulted]),
919            },
920            DExpr::ExternalSlot(field) => d::Stmt::Assign {
921                place: field,
922                value: Self::call_expr(Self::runtime_name("zdPtr"), vec![defaulted]),
923            },
924            DExpr::Plain(field) => d::Stmt::Assign {
925                place: field,
926                value: defaulted,
927            },
928        })
929    }
930
931    /// # Errors
932    /// As [`crate::default_d::member_default`]: a `@default` this
933    /// column cannot spell is refused rather than silently replaced by
934    /// Table 9's zero.
935    fn member_default(
936        ast: &Ast,
937        node: &MemberNode,
938        type_: Id<TypeNode>,
939        place: Self::Expr,
940    ) -> Result<Vec<Self::Stmt>, EncodeError> {
941        crate::default_d::member_default(ast, node, type_, inner(place))
942    }
943
944    /// `Mode.IDLE` — the enum's own scope, which is where the
945    /// materialiser declared the member. The trait's default answers a
946    /// bare prefixed name, which in D names nothing.
947    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
948        plain(d::Expr::Name(Ident::new(&format!(
949            "{}.{}",
950            d_type_name(ast, id),
951            field_name(enumerator)
952        ))))
953    }
954}
955
956/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
957/// that keeps two of them apart.
958///
959/// The tag names the routine a hoisted collection's codec is emitted
960/// under, and one body is emitted per name. With the bound left out,
961/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
962/// was declared first decides: measured on
963/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
964/// generated D held no `zd_over` check at all, so `a` accepted a
965/// stream of any length; with the members the other way round both got
966/// the bounded body, so `b` refused — and truncated — a length the IDL
967/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
968/// beside `wstring` collapse the same way, onto the trimming call.
969/// Either way the reader answered with a value the IDL does not
970/// describe and said nothing, which is why the bound is part of the
971/// name. The same suffix the Zig column carries.
972///
973/// An `array` needs nothing here: its `dims` are already in the tag,
974/// under the `Array` arm below.
975fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
976    match bound.expr() {
977        None => String::new(),
978        // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
979        // arm answers for an array of four `long`, and the two are
980        // different wire forms.
981        Some(expr) => match ast.eval_integral(expr) {
982            Ok(limit) => format!("_b{limit}"),
983            // As the array's `_x`: a bound that does not evaluate is
984            // still not the unbounded form, and the materialiser
985            // reports the expression that would not evaluate.
986            Err(_) => "_bx".to_string(),
987        },
988    }
989}
990
991/// The name a codec function takes for the type it codes: the IDL kind
992/// and not the D spelling, structurally for a collection and the
993/// flattened D name for a declaration — the same scheme the Go column
994/// carries, for the reasons its `type_tag` states.
995///
996/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
997#[must_use]
998pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
999    if depth > MAX_TYPE_DEPTH {
1000        return "unresolved".to_string();
1001    }
1002    let node = match ast.type_(id) {
1003        Ok(node) => node,
1004        Err(_) => return "unresolved".to_string(),
1005    };
1006    match &node.kind {
1007        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1008        TypeKind::String {
1009            width: CharWidth::Char,
1010            bound,
1011        } => format!("string{}", bound_tag(ast, bound)),
1012        TypeKind::String {
1013            width: CharWidth::WChar,
1014            bound,
1015        } => format!("wstring{}", bound_tag(ast, bound)),
1016        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1017        TypeKind::Sequence { element, bound } => match element.resolved() {
1018            Some(element) => format!(
1019                "seq{}_{}",
1020                bound_tag(ast, bound),
1021                type_tag(ast, element, depth + 1)
1022            ),
1023            None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1024        },
1025        TypeKind::Array { element, dims } => {
1026            let mut name = match element.resolved() {
1027                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1028                None => "arr_unresolved".to_string(),
1029            };
1030            for dim in dims {
1031                match ast.eval_integral(*dim) {
1032                    Ok(length) => name.push_str(&format!("_{length}")),
1033                    // No number rather than a zero: the name has to
1034                    // differ from a `[0]` array's, and the materialiser
1035                    // reports the dimension that would not evaluate.
1036                    Err(_) => name.push_str("_x"),
1037                }
1038            }
1039            name
1040        }
1041        TypeKind::Map { key, value, bound } => {
1042            let half = |half: Option<Id<TypeNode>>| match half {
1043                Some(target) => type_tag(ast, target, depth + 1),
1044                None => "unresolved".to_string(),
1045            };
1046            format!(
1047                "map{}_{}_{}",
1048                bound_tag(ast, bound),
1049                half(key.resolved()),
1050                half(value.resolved())
1051            )
1052        }
1053        // An alias is followed through: two IDL names for one shape are
1054        // one wire form, and two codecs for it would be two chances to
1055        // differ.
1056        TypeKind::Alias { aliased } => match aliased.resolved() {
1057            Some(target) => type_tag(ast, target, depth + 1),
1058            None => "unresolved".to_string(),
1059        },
1060        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1061            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1062            None => "box_unresolved".to_string(),
1063        },
1064        TypeKind::Structure { .. }
1065        | TypeKind::Union { .. }
1066        | TypeKind::Enumeration { .. }
1067        | TypeKind::Bitmask { .. }
1068        | TypeKind::Bitset { .. }
1069        | TypeKind::Valuetype { .. }
1070        | TypeKind::Native => d_type_name(ast, id),
1071        TypeKind::Any => "any".to_string(),
1072        TypeKind::ValueBase => "valuebase".to_string(),
1073        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1074        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1075    }
1076}