Skip to main content

zerodds_lang_rules/emit/
zig.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Zig backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as the columns before
6//! it, and none of them calls another. Where two columns disagree on an
7//! octet, one has misread the specification — which is the only reason
8//! to have a further one.
9//!
10//! # The five decisions this column turns on
11//!
12//! **Errors travel as error unions, and the seam writes the `try`.**
13//! Zig has no exceptions: a runtime operation that can refuse answers
14//! `zdwire.WireError!T`, every generated routine answers
15//! `zdwire.WireError!void` or `!T`, and this backend prepends `try` at
16//! each fallible call. Which operations are fallible is a contract
17//! between this file and `runtime/zig/zdwire.zig` — the two tables
18//! `FALLIBLE_EXEMPT_METHODS` and `FALLIBLE_STATICS` are that
19//! contract's edge, and a name moved on one side without the other is a
20//! Zig compile error (`expected error union type`), never a silent
21//! drop.
22//!
23//! **A member holds its wire width, under Zig's own names.** An IDL
24//! `long` is `i32`, a `char` is `u8`, a `wchar` `u16`, so
25//! [`CodecTarget::code_point`] is the identity. A `long double` is a
26//! real `f128` — the one column whose built-in set reaches it; the
27//! adapter's table carries the argument.
28//!
29//! **There is no one cast operator, so [`CodecTarget::cast`] chooses.**
30//! `@as(T, @intCast(v))` between integer widths, `@enumFromInt` into an
31//! enumeration (the walk casts a union label into its discriminator's
32//! type), `@bitCast` for the walk's `signed char` reading of a map key,
33//! and `zdwire.asI64`/`asU64` where the walk widens a value it cannot
34//! see the type of — an enumeration sample and a union overlay arm are
35//! the two, and a comptime `switch` on `@typeInfo` answers for both.
36//!
37//! **`@optional` is `?T` and `@external` is `*T`, and the read
38//! direction allocates.** A decoded `T` coerces into a `?T` on its own;
39//! a `*T` needs storage, which `zdwire.zdPtr(r.heap, …)` takes from the
40//! reader's allocator — the same arena everything else the reader
41//! materialises comes from.
42//!
43//! **Zig's unused-and-shadowing rules are settled in [`Zig::routine`],
44//! once.** An unused parameter and a pointless discard are both compile
45//! errors, and which one a body earns depends on the whole body; the
46//! routine assembler scans it and keeps exactly the discards that are
47//! needed. A counted loop renders inside its own block, so two
48//! sequential loops can share a counter name without Zig's shadowing
49//! error — that one lives in the renderer.
50
51use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
52use crate::adapter::zig::{field_name, zig_map_entry_name, zig_type_name, zig_value_type};
53use crate::encode::EncodeError;
54use zerodds_ast::{Ast, CharWidth, MemberNode, TypeKind, TypeNode};
55use zerodds_ast_core::Id;
56use zerodds_lang_cst::common::{Doc, Ident, Literal};
57use zerodds_lang_cst::zig;
58
59/// The Zig backend.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct Zig;
62
63/// How deep a chain of typedefs and collections is followed.
64const MAX_TYPE_DEPTH: usize = 64;
65
66/// The name the generated file imports the runtime under.
67const RUNTIME: &str = "zdwire";
68
69/// The type name [`CodecTarget::named`] is handed for C's signed
70/// `char`; the Zig member is a `u8` and the signed reading is
71/// `@as(i8, @bitCast(..))`.
72const SIGNED_CHAR: &str = "signed char";
73
74/// The reader and writer operations that answer plainly, everything
75/// else on a receiver being `WireError!T`.
76///
77/// One half of the fallibility contract with `runtime/zig/zdwire.zig`;
78/// the module documentation says why a mismatch is loud.
79const FALLIBLE_EXEMPT_METHODS: &[&str] = &[
80    "reader_status",
81    "reader_position",
82    "reader_remaining",
83    "reader_save",
84    "reader_restore",
85    "reader_refuse",
86    "reader_clear_discard",
87    "reader_begin_packed_element",
88    "reader_finish_packed_element",
89    "begin_packed_element",
90    "finish_packed_element",
91    "finish_pid_read",
92    "position",
93    "trim_utf8",
94    "trim_utf16",
95];
96
97/// The free functions of the runtime that answer `WireError!T`;
98/// everything else [`CodecTarget::runtime_static`] names answers
99/// plainly. The other half of the contract.
100const FALLIBLE_STATICS: &[&str] = &["utf16_units"];
101
102/// An expression, with the two properties the walk's seams cannot
103/// carry: whether it needs a `try`, and which indirection the place it
104/// names lives behind.
105#[derive(Debug, Clone)]
106pub enum ZigExpr {
107    /// An ordinary expression.
108    Plain(zig::Expr),
109    /// A call that answers `WireError!T`: `try` in a statement or
110    /// initialiser position, `(try …)` inside a larger expression.
111    Fallible(zig::Expr),
112    /// The `?T` field of an `@optional` member, as a place a decoded
113    /// `T` is about to be written into — the coercion is Zig's own.
114    OptionalSlot(zig::Expr),
115    /// The `*T` field of an `@external` member, likewise; the decoded
116    /// value goes through `zdwire.zdPtr` to get storage.
117    ExternalSlot(zig::Expr),
118}
119
120/// A Zig type, with the two properties [`CodecTarget::cast`] needs and
121/// the tree cannot carry: whether the type is an enumeration once
122/// typedefs are seen through, which decides `@enumFromInt` against
123/// `@intCast`, and whether a named type is an integer alias, which
124/// decides `@intCast` against a bare `@as`.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ZigType {
127    /// The type as written.
128    pub tree: zig::Type,
129    /// Whether a cast into it converts from an integer with
130    /// `@enumFromInt`.
131    pub enum_like: bool,
132    /// Whether the type names an integer under its own name — a
133    /// bitmask, whose Zig form is `pub const Flags = u8;`. The tree
134    /// says `Named("Flags")` and `@as(Flags, zd_bits)` on the walk's
135    /// sixty-four-bit holder is `expected type 'u8', found 'u64'`;
136    /// the narrowing has to be stated.
137    pub integer_alias: bool,
138}
139
140impl ZigType {
141    fn plain(tree: zig::Type) -> Self {
142        Self {
143            tree,
144            enum_like: false,
145            integer_alias: false,
146        }
147    }
148}
149
150/// The expression inside, whichever form it took; a fallible one is
151/// bracketed so `try` binds to the call and not to whatever expression
152/// it ends up inside.
153fn inner(expr: ZigExpr) -> zig::Expr {
154    match expr {
155        ZigExpr::Plain(e) | ZigExpr::OptionalSlot(e) | ZigExpr::ExternalSlot(e) => e,
156        ZigExpr::Fallible(e) => zig::Expr::Paren(Box::new(zig::Expr::try_(e))),
157    }
158}
159
160/// The expression with a bare `try` where one is due — an initialiser
161/// and an assignment right-hand side take it without the bracket.
162fn inner_bare(expr: ZigExpr) -> zig::Expr {
163    match expr {
164        ZigExpr::Plain(e) | ZigExpr::OptionalSlot(e) | ZigExpr::ExternalSlot(e) => e,
165        ZigExpr::Fallible(e) => zig::Expr::try_(e),
166    }
167}
168
169fn plain(e: zig::Expr) -> ZigExpr {
170    ZigExpr::Plain(e)
171}
172
173/// A local's name, moved off Zig's primitive namespace: the encode walk
174/// counts an array's dimensions with `i0`, `i1`, …, which Zig parses as
175/// arbitrary-width integer types and refuses as locals. The prefix and
176/// not `escape_decl`'s trailing underscore, because the lexer reads
177/// `i0_` as a malformed width too ("primitive integer type 'i0_' has
178/// leading zero", Zig 0.14.1). Applied at every place a local is
179/// written or read, so the two spellings cannot part.
180fn local_name(text: &str) -> String {
181    if zig::is_primitive_name(text) {
182        return format!("zd_{text}");
183    }
184    text.to_string()
185}
186
187impl Zig {
188    /// A runtime name, qualified: `zdwire.X`.
189    fn runtime_name(name: &str) -> zig::Expr {
190        zig::Expr::dot(zig::Expr::Name(Ident::new(RUNTIME)), name)
191    }
192
193    /// A runtime operation's Zig spelling: `write_u32` is `writeU32`,
194    /// each `_`-separated segment after the first with its first letter
195    /// raised — the names `runtime/zig` publishes, because that runtime
196    /// was written against this conversion.
197    fn camel(method: &str) -> String {
198        let mut out = String::new();
199        for (index, segment) in method.split('_').enumerate() {
200            if index == 0 {
201                out.push_str(segment);
202                continue;
203            }
204            let mut chars = segment.chars();
205            if let Some(first) = chars.next() {
206                out.extend(first.to_uppercase());
207                out.push_str(chars.as_str());
208            }
209        }
210        out
211    }
212
213    fn text(value: &str) -> ZigExpr {
214        plain(zig::Expr::Literal(Literal::Str(value.to_string())))
215    }
216
217    /// One value of a runtime enumeration: `zdwire.Status.ok`.
218    fn runtime_enum(type_: &str, value: &str) -> ZigExpr {
219        plain(zig::Expr::dot(Self::runtime_name(type_), value))
220    }
221
222    /// `r.heap` — the reader's arena, which is where everything the
223    /// read direction materialises comes from.
224    fn reader_heap() -> zig::Expr {
225        zig::Expr::dot(zig::Expr::Name(Ident::new("r")), "heap")
226    }
227
228    /// Whether the resolved type behind `id` is an enumeration.
229    fn is_enumeration(ast: &Ast, id: Id<TypeNode>) -> bool {
230        super::resolves_to_enumeration(ast, id)
231    }
232
233    /// Whether the resolved type behind `id` is a bit mask, whose Zig
234    /// form is a `pub const` alias of an unsigned integer rather than a
235    /// type of its own.
236    fn is_bitmask(ast: &Ast, id: Id<TypeNode>) -> bool {
237        let mut current = id;
238        for _ in 0..MAX_TYPE_DEPTH {
239            match ast.type_(current).map(|node| node.kind.clone()) {
240                Ok(TypeKind::Bitmask { .. }) => return true,
241                Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
242                    Some(target) => current = target,
243                    None => return false,
244                },
245                _ => return false,
246            }
247        }
248        false
249    }
250
251    /// Whether `tree` is a fixed-width integer or `usize` — the types a
252    /// cast reaches with `@intCast`.
253    fn is_integer_type(tree: &zig::Type) -> bool {
254        match tree {
255            zig::Type::Primitive(primitive) => matches!(
256                primitive,
257                zig::Primitive::I8
258                    | zig::Primitive::I16
259                    | zig::Primitive::I32
260                    | zig::Primitive::I64
261                    | zig::Primitive::U8
262                    | zig::Primitive::U16
263                    | zig::Primitive::U32
264                    | zig::Primitive::U64
265            ),
266            zig::Type::Named(name) => name.as_str() == "usize",
267            _ => false,
268        }
269    }
270}
271
272impl CodecTarget for Zig {
273    type Expr = ZigExpr;
274    type Stmt = zig::Stmt;
275    type Block = Vec<zig::Stmt>;
276    type Type = ZigType;
277    type Decl = zig::Decl;
278
279    // -- types --------------------------------------------------------
280
281    /// The fixed-width Zig type each scalar local takes. `Index` is
282    /// `u32` — a sequence length is a `uint32` on the wire, and a `u32`
283    /// coerces into a slice index — and `Size` is `usize`, which is
284    /// what the reader's positions answer in.
285    fn scalar(kind: Scalar) -> Self::Type {
286        ZigType::plain(match kind {
287            Scalar::Bool => zig::Type::Primitive(zig::Primitive::Bool),
288            Scalar::Char | Scalar::UInt8 => zig::Type::Primitive(zig::Primitive::U8),
289            Scalar::Int8 => zig::Type::Primitive(zig::Primitive::I8),
290            Scalar::Int16 => zig::Type::Primitive(zig::Primitive::I16),
291            Scalar::UInt16 => zig::Type::Primitive(zig::Primitive::U16),
292            Scalar::Int32 => zig::Type::Primitive(zig::Primitive::I32),
293            Scalar::UInt32 | Scalar::Index => zig::Type::Primitive(zig::Primitive::U32),
294            Scalar::Int64 => zig::Type::Primitive(zig::Primitive::I64),
295            Scalar::UInt64 => zig::Type::Primitive(zig::Primitive::U64),
296            Scalar::Float => zig::Type::Primitive(zig::Primitive::F32),
297            Scalar::Double => zig::Type::Primitive(zig::Primitive::F64),
298            // The real binary128; the adapter's table says why this is
299            // the one column that has it.
300            Scalar::LongDouble => zig::Type::Primitive(zig::Primitive::F128),
301            Scalar::Size => zig::Type::named("usize"),
302        })
303    }
304
305    fn named(name: &str) -> Self::Type {
306        ZigType::plain(zig::Type::named(name))
307    }
308
309    fn nothing() -> Self::Type {
310        ZigType::plain(zig::Type::Primitive(zig::Primitive::Void))
311    }
312
313    /// The runtime's handles, qualified.
314    fn handle(kind: Handle) -> Self::Type {
315        ZigType::plain(zig::Type::named(match kind {
316            Handle::DHeader => "zdwire.DHeader",
317            Handle::EmHeader => "zdwire.EmHeader",
318            Handle::Pid => "zdwire.Pid",
319            Handle::TryConstruct => "zdwire.TryConstruct",
320            Handle::ReaderMark => "zdwire.ReaderMark",
321            Handle::PidRead => "zdwire.PidRead",
322            Handle::EmHeaderRead => "zdwire.EmHeaderRead",
323        }))
324    }
325
326    /// The member already is the code unit — `u8` for a `char`, `u16`
327    /// for a `wchar` — so there is nothing to convert.
328    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
329        value
330    }
331
332    /// `@intFromEnum(v)` for an enumeration, the value itself for
333    /// anything else. `adapter/zig.rs` declares an enumeration as
334    /// `enum(i32)` with every enumerator's declared value written out,
335    /// and Zig's `<` refuses a pair of them ("operator < not allowed
336    /// for type 'main.Colour'", measured in the matrix refresh of
337    /// 2026-10-02 on `map-key-enum` under the Fast DDS and OpenDDS
338    /// dialects); the tag is
339    /// [`zerodds_ast_layout::MapKeyOrdering::EnumeratorValue`]'s order.
340    /// The union overlay asks the same seam for every integer arm, so a
341    /// type that is not an enumeration passes through.
342    fn enumerator_ordinal(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
343        if !Self::is_enumeration(ast, id) {
344            return value;
345        }
346        plain(zig::Expr::call("@intFromEnum", vec![inner(value)]))
347    }
348
349    /// `zdwire.boolLess(a, b)` — `false` before `true`. An override
350    /// because Zig's own `<` refuses a pair of booleans outright
351    /// ("operator < not allowed for type 'bool'"), which is exactly the
352    /// class of target the trait documents this seam for.
353    fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
354        plain(zig::Expr::Call {
355            func: Box::new(Self::runtime_name("boolLess")),
356            args: vec![inner(left), inner(right)],
357        })
358    }
359
360    /// The runtime's policy values: integers and strings where
361    /// `runtime/zig` takes them so, and the runtime's own enumerations
362    /// for the enumerated kinds. One arm per constant and no catch-all.
363    fn constant(which: Constant) -> Self::Expr {
364        match which {
365            Constant::WcharWidth2 => plain(zig::Expr::int(2)),
366            Constant::WcharWidth4 => plain(zig::Expr::int(4)),
367            Constant::WcharFixed => Self::text("fixed"),
368            Constant::WcharGiopLengthPrefixed => Self::text("giop-length-prefixed"),
369            Constant::WstringUnitsStream => Self::text("stream"),
370            Constant::WstringUnitsBigEndianAlways => Self::text("big-endian-always"),
371            Constant::WstringUnitsByteOrderMark => Self::text("byte-order-mark"),
372            Constant::WstringUnitsHostUnswapped => Self::text("host-unswapped"),
373            Constant::WstringSpecification => Self::text("specification"),
374            Constant::WstringCharacterCount => Self::text("character-count"),
375            Constant::WstringCharacterCountTerminated => Self::text("character-count-terminated"),
376            Constant::WstringSpecificationWideUnits => Self::text("specification-wide-units"),
377            Constant::WstringRosTypeSupport => Self::text("ros-type-support"),
378            Constant::WstringOctetsTerminated2 => Self::text("octets/terminated/2"),
379            Constant::WstringOctetsTerminated4 => Self::text("octets/terminated/4"),
380            Constant::WstringCharactersTerminated2 => Self::text("characters/terminated/2"),
381            Constant::FixedDeclared => Self::text("declared"),
382            Constant::FixedValuePrecision => Self::text("value-precision"),
383            Constant::FixedValueStringDigits => Self::text("value-string-digits"),
384            Constant::ParameterMember => Self::runtime_enum("ParameterRole", "member"),
385            Constant::ParameterOptional => Self::runtime_enum("ParameterRole", "optional"),
386            Constant::TryConstructDiscard => Self::runtime_enum("TryConstruct", "discard"),
387            Constant::TryConstructTrim => Self::runtime_enum("TryConstruct", "trim"),
388            Constant::TryConstructUseDefault => Self::runtime_enum("TryConstruct", "use_default"),
389            Constant::StatusOk => Self::runtime_enum("Status", "ok"),
390            Constant::StatusDiscarded => Self::runtime_enum("Status", "discarded"),
391            Constant::StatusImpossibleLength => Self::runtime_enum("Status", "impossible_length"),
392            Constant::StatusOutOfRange => Self::runtime_enum("Status", "out_of_range"),
393            Constant::StatusTruncated => Self::runtime_enum("Status", "truncated"),
394        }
395    }
396
397    /// The unsigned type wide enough for `bits` of a bit set — the same
398    /// answer the materialiser's `zig_holder` gives its fields.
399    fn unsigned_holder(bits: u16) -> Self::Type {
400        ZigType::plain(zig::Type::Primitive(crate::adapter::zig::zig_holder(bits)))
401    }
402
403    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
404        // A type with no Zig form was refused by the materialiser
405        // before a codec walk ran, and `void` makes the impossible fail
406        // in the Zig compiler, loudly, rather than compile into a
407        // plausible member.
408        let tree = match zig_value_type(ast, id, "codec") {
409            Ok(tree) => tree,
410            Err(_) => zig::Type::Primitive(zig::Primitive::Void),
411        };
412        ZigType {
413            tree,
414            enum_like: Self::is_enumeration(ast, id),
415            integer_alias: Self::is_bitmask(ast, id),
416        }
417    }
418
419    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
420        type_tag(ast, id, 0)
421    }
422
423    /// The entry name the materialiser declared, and a distinct one
424    /// per type when its tag walk refuses: the slot keeps two refused
425    /// entries from collapsing onto one declaration, which would merge
426    /// two map types under one name.
427    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
428        zig_map_entry_name(ast, id).unwrap_or_else(|_| format!("ZdUnresolvedEntry{}", id.slot()))
429    }
430
431    /// Nothing: a declared type's Zig name is already flat — the
432    /// adapter folds the modules in — and an enumerator lives in its
433    /// enum's own scope, which [`CodecTarget::enumerator_constant`]'s
434    /// override reaches.
435    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
436        String::new()
437    }
438
439    /// The escape and nothing else, exactly as the materialiser spells
440    /// a field.
441    fn escape(name: &str) -> String {
442        field_name(name)
443    }
444
445    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
446    fn inherited_path(depth: usize) -> Option<String> {
447        Some(format!("{}.", field_name("parent")).repeat(depth))
448    }
449
450    // -- expressions ---------------------------------------------------
451
452    fn name(text: &str) -> Self::Expr {
453        plain(zig::Expr::Name(Ident::new(&local_name(text))))
454    }
455
456    fn int(value: i128) -> Self::Expr {
457        plain(zig::Expr::int(value))
458    }
459
460    /// The raw tokens the walk escapes through [`Literal::Int`],
461    /// rewritten into the tree's own boolean where they are one.
462    fn literal(value: Literal) -> Self::Expr {
463        let Literal::Int(text) = &value else {
464            return plain(zig::Expr::Literal(value));
465        };
466        match text.as_str() {
467            "true" => plain(zig::Expr::Literal(Literal::Bool(true))),
468            "false" => plain(zig::Expr::Literal(Literal::Bool(false))),
469            _ => plain(zig::Expr::Literal(value)),
470        }
471    }
472
473    /// The number, unsuffixed: a Zig integer literal is a
474    /// `comptime_int` of arbitrary precision and coerces to whatever
475    /// the position wants, so no width suffix exists or is needed.
476    fn wide_int(value: i128) -> Self::Expr {
477        plain(zig::Expr::int(value))
478    }
479
480    /// As [`CodecTarget::wide_int`]: a bare `comptime_int`.
481    fn unsigned_int(value: i128) -> Self::Expr {
482        plain(zig::Expr::int(value))
483    }
484
485    /// The bare number here too; the one operation a `comptime_int`
486    /// refuses — the `~` a bitmask's declared-bit check complements —
487    /// is given its width at the complement, in [`Zig::unary`].
488    fn wide_unsigned(value: u64) -> Self::Expr {
489        plain(zig::Expr::int(i128::from(value)))
490    }
491
492    fn wide_signed(value: i128) -> Self::Expr {
493        plain(zig::Expr::int(value))
494    }
495
496    /// `v.x` — and `v._d` as it stands: Zig keeps the IDL-facing
497    /// spelling, underscore and all.
498    fn member(base: &str, field: &str) -> Self::Expr {
499        plain(zig::Expr::dot(zig::Expr::Name(Ident::new(base)), field))
500    }
501
502    fn whole(base: &str) -> Self::Expr {
503        plain(zig::Expr::Name(Ident::new(&local_name(base))))
504    }
505
506    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
507        plain(zig::Expr::dot(inner(value), field))
508    }
509
510    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
511        plain(zig::Expr::Index {
512            value: Box::new(inner(value)),
513            index: Box::new(inner(at)),
514        })
515    }
516
517    /// `@as(u32, @intCast(v.len))`, or the bound where the materialiser
518    /// fixed one: `.len` is a `usize` and a sequence's count is a
519    /// `uint32` on the wire.
520    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
521        match fixed {
522            Some(bound) => plain(zig::Expr::int(i128::from(bound))),
523            None => plain(zig::Expr::as_(
524                zig::Type::Primitive(zig::Primitive::U32),
525                zig::Expr::call(
526                    "@intCast",
527                    vec![zig::Expr::dot(zig::Expr::Name(Ident::new(base)), "len")],
528                ),
529            )),
530        }
531    }
532
533    /// The slice itself: a Zig slice is the elements.
534    fn elements(base: &str) -> Self::Expr {
535        plain(zig::Expr::Name(Ident::new(base)))
536    }
537
538    /// `v.x` — a union is a struct carrying its branches directly.
539    fn case_member(base: &str, field: &str) -> Self::Expr {
540        Self::member(base, field)
541    }
542
543    /// `entry.key` and `entry.value`, the two fields the materialiser
544    /// gives a map's entry struct.
545    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
546        plain(zig::Expr::dot(
547            inner(entry),
548            if key { "key" } else { "value" },
549        ))
550    }
551
552    fn absent() -> Self::Expr {
553        plain(zig::Expr::Literal(Literal::Null))
554    }
555
556    /// Nothing: a Zig slice and string always exist once the runtime
557    /// answered — an allocation failure is an error the `try` already
558    /// propagated — so there is no absent state for the guard to test.
559    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
560        None
561    }
562
563    /// `v.x.?` — the value a `?T` holds, given that it holds one. Only
564    /// ever reached inside a branch the presence test guarded, where
565    /// the unwrap cannot trip.
566    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
567        plain(zig::Expr::dot(inner(Self::member(base, field)), "?"))
568    }
569
570    /// `v.x != null`.
571    fn present(base: &str, field: &str) -> Self::Expr {
572        plain(zig::Expr::Binary {
573            op: "!=",
574            lhs: Box::new(inner(Self::member(base, field))),
575            rhs: Box::new(zig::Expr::Literal(Literal::Null)),
576        })
577    }
578
579    /// Nothing on the way in — assigning the value is what makes the
580    /// member present, the `T` to `?T` coercion being Zig's own — and
581    /// `v.x = null;` on the way out.
582    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
583        if present {
584            return Vec::new();
585        }
586        vec![zig::Stmt::Assign {
587            place: inner(Self::member(base, field)),
588            value: zig::Expr::Literal(Literal::Null),
589        }]
590    }
591
592    /// The marked place; the decoded `T` coerces into the `?T` field on
593    /// assignment, so no room has to be made first.
594    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
595        (
596            Vec::new(),
597            ZigExpr::OptionalSlot(inner(Self::member(base, field))),
598        )
599    }
600
601    /// `true`: an `@external` member is a pointer, and a member on a cycle needs
602    /// the same: `?T` stores its `T` inline.
603    /// [`crate::adapter::held_externally`] is the rule the adapter and
604    /// the defaulter ask as well.
605    fn holds_a_cycle_externally() -> bool {
606        true
607    }
608
609    /// `v.x.*` — the pointer dereferenced — and `v.x.?.*` where the
610    /// member is also `@optional`: the optional carries the absence and
611    /// the pointer the indirection, in that order.
612    fn external_value(
613        _ast: &Ast,
614        _id: Id<TypeNode>,
615        base: &str,
616        field: &str,
617        optional: bool,
618    ) -> Self::Expr {
619        let mut place = inner(Self::member(base, field));
620        if optional {
621            place = zig::Expr::dot(place, "?");
622        }
623        plain(zig::Expr::dot(place, "*"))
624    }
625
626    /// The marked place; [`Zig::read_call`] routes the decoded value
627    /// through `zdwire.zdPtr`, and the `*T` it answers coerces into a
628    /// `?*T` where the member is also `@optional`.
629    fn external_destination(
630        base: &str,
631        field: &str,
632        _optional: bool,
633    ) -> (Vec<Self::Stmt>, Self::Expr) {
634        (
635            Vec::new(),
636            ZigExpr::ExternalSlot(inner(Self::member(base, field))),
637        )
638    }
639
640    /// `v.x.*` for a branch too: the union arm's field is the same `*T`
641    /// a structure's `@external` member is. No union member is
642    /// `@optional` (DDS-XTypes 1.3 §7.2.2.4.4.4.7), so no `.?` appears.
643    fn external_case_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
644        plain(zig::Expr::dot(inner(Self::case_member(base, field)), "*"))
645    }
646
647    /// The marked place, exactly as [`Zig::external_destination`]
648    /// answers for a structure member.
649    fn external_case_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
650        (
651            Vec::new(),
652            ZigExpr::ExternalSlot(inner(Self::case_member(base, field))),
653        )
654    }
655
656    /// `v.x != null`: the member is `?*T` wherever this is asked, and
657    /// the optional is what carries the absence.
658    fn external_present(base: &str, field: &str) -> Self::Expr {
659        Self::present(base, field)
660    }
661
662    /// Nothing on the way in, `v.x = null;` on the way out — the same
663    /// answer the plain optional gives, because the absence lives in
664    /// the same `?`.
665    fn external_set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
666        Self::set_present(base, field, present)
667    }
668
669    /// The operators, with the two boolean connectives translated:
670    /// the walk spells `&&` and `||` and Zig spells `and` and `or`.
671    /// `=` never reaches a rendered expression — [`Zig::perform`] turns
672    /// it into an assignment statement.
673    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
674        let spelled = match op {
675            "&&" => "and",
676            "||" => "or",
677            other => other,
678        };
679        plain(zig::Expr::Binary {
680            op: spelled,
681            lhs: Box::new(inner(lhs)),
682            rhs: Box::new(inner(rhs)),
683        })
684    }
685
686    /// The operator as given, with one rewrite: `~` on a bare integer
687    /// literal becomes `~@as(u64, ..)`, because a `comptime_int` has no
688    /// width to complement — the one place the walk builds that is the
689    /// bitmask reader's declared-bit check, whose other operand is the
690    /// sixty-four-bit holder.
691    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
692        let operand = inner(value);
693        if op == "~" {
694            if let zig::Expr::Literal(Literal::Int(_)) = &operand {
695                return plain(zig::Expr::Unary {
696                    op: "~",
697                    value: Box::new(zig::Expr::as_(
698                        zig::Type::Primitive(zig::Primitive::U64),
699                        operand,
700                    )),
701                });
702            }
703        }
704        plain(zig::Expr::Unary {
705            op,
706            value: Box::new(operand),
707        })
708    }
709
710    fn paren(value: Self::Expr) -> Self::Expr {
711        plain(zig::Expr::Paren(Box::new(inner(value))))
712    }
713
714    /// The conversion, spelled by what it converts into — the module
715    /// documentation carries the table. A `bool` target with an integer
716    /// literal becomes the boolean literal, as in the sibling columns.
717    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
718        let operand = inner(value);
719        if type_.tree == zig::Type::named(SIGNED_CHAR) {
720            return plain(zig::Expr::as_(
721                zig::Type::Primitive(zig::Primitive::I8),
722                zig::Expr::call("@bitCast", vec![operand]),
723            ));
724        }
725        if type_.tree == zig::Type::Primitive(zig::Primitive::Bool) {
726            if let zig::Expr::Literal(Literal::Int(text)) = &operand {
727                return plain(zig::Expr::Literal(Literal::Bool(text != "0")));
728            }
729        }
730        if type_.enum_like {
731            return plain(zig::Expr::as_(
732                type_.tree,
733                zig::Expr::call("@enumFromInt", vec![operand]),
734            ));
735        }
736        if type_.tree == zig::Type::Primitive(zig::Primitive::I64) {
737            return plain(zig::Expr::Call {
738                func: Box::new(Self::runtime_name("asI64")),
739                args: vec![operand],
740            });
741        }
742        if type_.tree == zig::Type::Primitive(zig::Primitive::U64) {
743            return plain(zig::Expr::Call {
744                func: Box::new(Self::runtime_name("asU64")),
745                args: vec![operand],
746            });
747        }
748        if Self::is_integer_type(&type_.tree) || type_.integer_alias {
749            // A literal coerces on its own; a runtime value states the
750            // narrowing out loud.
751            if let zig::Expr::Literal(Literal::Int(_)) = &operand {
752                return plain(zig::Expr::as_(type_.tree, operand));
753            }
754            return plain(zig::Expr::as_(
755                type_.tree,
756                zig::Expr::call("@intCast", vec![operand]),
757            ));
758        }
759        plain(zig::Expr::as_(type_.tree, operand))
760    }
761
762    /// The value, unchanged: Zig hands a slice by header, an array and
763    /// a struct by value.
764    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
765        value
766    }
767
768    /// A call to a generated codec — fallible, so the seam owns the
769    /// `try`. `strcmp` is the runtime's, which spells C11 §7.24.4's
770    /// unsigned-octet order over two UTF-8 slices, and answers plainly.
771    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
772        let arguments: Vec<zig::Expr> = args.into_iter().map(inner).collect();
773        if callee == "strcmp" {
774            return plain(zig::Expr::Call {
775                func: Box::new(Self::runtime_name("strcmp")),
776                args: arguments,
777            });
778        }
779        ZigExpr::Fallible(zig::Expr::call(callee, arguments))
780    }
781
782    /// `w.writeU32(x)` — a method on the writer or the reader, marked
783    /// fallible unless the contract's table exempts it.
784    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
785        let call = zig::Expr::Call {
786            func: Box::new(zig::Expr::dot(
787                zig::Expr::Name(Ident::new(receiver)),
788                &Self::camel(method),
789            )),
790            args: args.into_iter().map(inner).collect(),
791        };
792        if FALLIBLE_EXEMPT_METHODS.contains(&method) {
793            return plain(call);
794        }
795        ZigExpr::Fallible(call)
796    }
797
798    /// A free function of the runtime, qualified — except
799    /// `pid_is_user_member`, which is a method on the header the reader
800    /// handed back.
801    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
802        if method == "pid_is_user_member" {
803            let mut arguments = args.into_iter().map(inner);
804            if let Some(header) = arguments.next() {
805                return plain(zig::Expr::Call {
806                    func: Box::new(zig::Expr::dot(header, "isUserMember")),
807                    args: Vec::new(),
808                });
809            }
810            // The walk always hands the header; a call without one is a
811            // walk that changed under this file, and a function no
812            // scope declares is the loudest thing Zig can say here.
813            return plain(zig::Expr::Call {
814                func: Box::new(Self::runtime_name("isUserMember")),
815                args: Vec::new(),
816            });
817        }
818        let call = zig::Expr::Call {
819            func: Box::new(Self::runtime_name(&Self::camel(method))),
820            args: args.into_iter().map(inner).collect(),
821        };
822        if FALLIBLE_STATICS.contains(&method) {
823            return ZigExpr::Fallible(call);
824        }
825        plain(call)
826    }
827
828    /// `runtime/zig` carries the three HDDS axes: `beginPackedElement` /
829    /// `finishPackedElement` on the writer and `readerBeginPackedElement`
830    /// / `readerFinishPackedElement` on the reader, the DHEADER pair for
831    /// a sequence element's own length, and `writeWstringUtf8Octets` /
832    /// `writeWstringUtf8OctetsBounded` / `readWstringUtf8Octets`.
833    fn carries_hdds_axes() -> bool {
834        true
835    }
836
837    // -- statements ----------------------------------------------------
838
839    /// `var name: T = value;` — the type written, so a reader of the
840    /// generated file sees the width. Everything starts as `var`;
841    /// [`Zig::routine`]'s assembly pass turns what the body never
842    /// assigns into `const`, because Zig refuses each where the other
843    /// is meant.
844    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
845        zig::Stmt::Var {
846            name: Ident::new(&local_name(name)),
847            type_: Some(type_.tree),
848            value: value.map(inner_bare),
849            mutable: true,
850        }
851    }
852
853    /// An expression evaluated for its effect — with the `try` a
854    /// fallible one carries, and an assignment the walk built as its
855    /// own statement.
856    fn perform(value: Self::Expr) -> Self::Stmt {
857        match value {
858            ZigExpr::Plain(zig::Expr::Binary { op: "=", lhs, rhs }) => zig::Stmt::Assign {
859                place: *lhs,
860                value: *rhs,
861            },
862            ZigExpr::Fallible(e) => zig::Stmt::Expr(zig::Expr::try_(e)),
863            other => zig::Stmt::Expr(inner(other)),
864        }
865    }
866
867    /// `return v;`: a Zig decoder answers with the sample it built, and
868    /// the error union wraps it on its own.
869    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
870        zig::Stmt::Return(Some(match value {
871            Some(value) => inner(value),
872            None => zig::Expr::Name(Ident::new("v")),
873        }))
874    }
875
876    fn when(
877        condition: Self::Expr,
878        then: Self::Block,
879        otherwise: Option<Self::Block>,
880    ) -> Self::Stmt {
881        zig::Stmt::If {
882            condition: inner(condition),
883            then,
884            otherwise: otherwise.filter(|body| !body.is_empty()),
885        }
886    }
887
888    /// The counter block the renderer scopes: every bound the walk
889    /// hands over is a `u32` count off the wire or a literal.
890    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
891        zig::Stmt::CountUp {
892            counter: Ident::new(&local_name(counter)),
893            type_: Box::new(zig::Type::Primitive(zig::Primitive::U32)),
894            bound: Box::new(inner(bound)),
895            body,
896        }
897    }
898
899    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
900        zig::Stmt::While {
901            condition: inner(condition),
902            body,
903        }
904    }
905
906    fn leave() -> Self::Stmt {
907        zig::Stmt::Break
908    }
909
910    fn note(doc: Doc) -> Self::Stmt {
911        zig::Stmt::Comment(doc)
912    }
913
914    /// `_ = try …;` — Zig's spelling of a value read for its effect.
915    fn drop_value(value: Self::Expr) -> Self::Stmt {
916        zig::Stmt::DiscardExpr(inner_bare(value))
917    }
918
919    /// `_ = name;` — kept only where [`Zig::routine`]'s assembly pass
920    /// finds no other mention of the name: Zig refuses both an unused
921    /// parameter and a pointless discard, and which of the two a body
922    /// earns is the whole body's question.
923    fn mark_used(name: &str) -> Vec<Self::Stmt> {
924        vec![zig::Stmt::Discard(Ident::new(name))]
925    }
926
927    /// `place = try zd_dec_T(r, behaviour);` — and through
928    /// `zdwire.zdPtr` where the place is an `@external` member's field:
929    /// the decoder answers a `T` and the field holds a pointer. An
930    /// `@optional` member's `?T` takes the bare `T` by coercion.
931    fn read_call(
932        callee: &str,
933        place: Self::Expr,
934        _form: Handoff,
935        behaviour: Self::Expr,
936    ) -> Self::Stmt {
937        let decoded = zig::Expr::call(
938            callee,
939            vec![zig::Expr::Name(Ident::new("r")), inner(behaviour)],
940        );
941        match place {
942            ZigExpr::ExternalSlot(field) => zig::Stmt::Assign {
943                place: field,
944                value: zig::Expr::try_(zig::Expr::Call {
945                    func: Box::new(Self::runtime_name("zdPtr")),
946                    args: vec![Self::reader_heap(), zig::Expr::try_(decoded)],
947                }),
948            },
949            ZigExpr::OptionalSlot(field) | ZigExpr::Plain(field) | ZigExpr::Fallible(field) => {
950                zig::Stmt::Assign {
951                    place: field,
952                    value: zig::Expr::try_(decoded),
953                }
954            }
955        }
956    }
957
958    /// `v = try zd_dec_T(r, zd_tc);` — a `typedef`, handed straight on.
959    ///
960    /// The trait's default is the C shape, `zd_dec_T(r, v, zd_tc)` with
961    /// the value as an out-parameter, and a Zig decoder takes two
962    /// arguments and answers the value: `error: expected 2 argument(s),
963    /// found 3` on `resolve-typedef-chain-three` and
964    /// `sequence-of-typedef`. Every column whose decoders answer a value
965    /// rather than fill an out-parameter — C#, D, Go, Java, Python,
966    /// TypeScript — overrides it the same way; C and C++ keep the
967    /// default.
968    fn read_forward(callee: &str) -> Self::Stmt {
969        Self::read_call(
970            callee,
971            Self::whole("v"),
972            Handoff::Value,
973            Self::name("zd_tc"),
974        )
975    }
976
977    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
978        statements
979    }
980
981    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
982        block
983    }
984
985    /// `v = try r.heap.alloc(T, n);`, and the fill after it: the slots
986    /// are storage the moment the allocator answers, and the fill
987    /// assigns every one of them before anything reads it.
988    fn make_room(
989        base: &str,
990        element: Self::Type,
991        count: Self::Expr,
992        _assigned_whole: bool,
993        fill: Vec<Self::Stmt>,
994    ) -> Vec<Self::Stmt> {
995        let mut out = vec![zig::Stmt::Assign {
996            place: zig::Expr::Name(Ident::new(base)),
997            value: zig::Expr::try_(zig::Expr::Call {
998                func: Box::new(zig::Expr::dot(Self::reader_heap(), "alloc")),
999                args: vec![zig::Expr::type_(element.tree), inner(count)],
1000            }),
1001        }];
1002        out.extend(fill);
1003        out
1004    }
1005
1006    /// `v = v[0..count];` — §7.3.1.2.1.12.3's TRIM, applied after every
1007    /// element has been read: a Zig slice shortens by re-slicing.
1008    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1009        vec![zig::Stmt::Assign {
1010            place: zig::Expr::Name(Ident::new(base)),
1011            value: zig::Expr::Index {
1012                value: Box::new(zig::Expr::Name(Ident::new(base))),
1013                index: Box::new(zig::Expr::Binary {
1014                    op: "..",
1015                    lhs: Box::new(zig::Expr::int(0)),
1016                    rhs: Box::new(inner(count)),
1017                }),
1018            },
1019        }]
1020    }
1021
1022    /// The subscript, with nothing around it: a Zig slice element is a
1023    /// real place at every element type.
1024    fn sequence_element(
1025        base: &str,
1026        index: Self::Expr,
1027        _element: Self::Type,
1028    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1029        (
1030            Vec::new(),
1031            Self::index(Self::elements(base), index),
1032            Vec::new(),
1033        )
1034    }
1035
1036    // -- assembly ------------------------------------------------------
1037
1038    /// `fn zd_enc_T(w: *zdwire.Writer, v: T) zdwire.WireError!void` and
1039    /// `fn zd_dec_T(r: *zdwire.Reader, zd_tc: zdwire.TryConstruct)
1040    /// zdwire.WireError!T`.
1041    ///
1042    /// A forward declaration is answered with nothing: a Zig
1043    /// container-level name is in scope for the whole file however far
1044    /// down it is written, so a recursive type needs no declaration
1045    /// ahead of its body.
1046    ///
1047    /// This is also where Zig's unused-and-mutability rules are
1048    /// settled, because both are properties of the whole body: every
1049    /// local the body never assigns becomes `const`, a parameter the
1050    /// body never mentions gets a `_ = name;`, and a discard the walk
1051    /// asked for is kept only when the name has no other mention —
1052    /// Zig refuses a pointless discard as loudly as an unused
1053    /// parameter.
1054    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1055        let body = spec.body?;
1056        let mut params = vec![zig::FnParam {
1057            name: Ident::new(if spec.writing { "w" } else { "r" }),
1058            type_: zig::Type::pointer(zig::Type::named(if spec.writing {
1059                "zdwire.Writer"
1060            } else {
1061                "zdwire.Reader"
1062            })),
1063        }];
1064        if spec.writing {
1065            params.push(zig::FnParam {
1066                name: Ident::new("v"),
1067                type_: spec.sample.tree.clone(),
1068            });
1069        }
1070        for (type_, name) in spec.extra {
1071            params.push(zig::FnParam {
1072                name: Ident::new(&name),
1073                type_: type_.tree,
1074            });
1075        }
1076        let error = zig::Type::named("zdwire.WireError");
1077        let (returns, mut body) = if spec.writing {
1078            (
1079                zig::Type::error_union(error, zig::Type::Primitive(zig::Primitive::Void)),
1080                body,
1081            )
1082        } else {
1083            // The sample starts as Table 9's value, through the
1084            // generated defaulter, so every position the stream does
1085            // not carry is already what the clause says it is.
1086            let defaulter = crate::default_zig::defaulter_from_decoder(&spec.name);
1087            let mut statements = vec![zig::Stmt::Var {
1088                name: Ident::new("v"),
1089                type_: Some(spec.sample.tree.clone()),
1090                value: Some(zig::Expr::try_(zig::Expr::call(
1091                    &defaulter,
1092                    vec![Self::reader_heap()],
1093                ))),
1094                mutable: true,
1095            }];
1096            statements.extend(body);
1097            statements.push(zig::Stmt::Return(Some(zig::Expr::Name(Ident::new("v")))));
1098            (zig::Type::error_union(error, spec.sample.tree), statements)
1099        };
1100        let assigned = assigned_roots(&body);
1101        fix_mutability(&mut body, &assigned);
1102        settle_discards(&mut body, &params);
1103        Some(zig::Decl::Func(zig::FuncDecl {
1104            doc: spec.doc,
1105            // `pub`, because the emission is a module and a codec that
1106            // cannot be called from the file that imports it is not one:
1107            // a Zig declaration without it is private to its file (Zig
1108            // language reference, "Keyword pub"), so
1109            // `@import("gen.zig").zd_enc_Max` was an error. Measured:
1110            // `zig_endpoint_sync`/`_async` in `crates/endpoint-e2e`
1111            // build the generated module beside an app that calls
1112            // `gen.zd_enc_Ping`, and Zig answers `'zd_enc_Ping' is not
1113            // marked 'pub'`. Uniform over every routine rather than only
1114            // the top-level ones: `pub` at container level says nothing
1115            // about the file itself, and a helper the walk hoisted is
1116            // reachable from the same import.
1117            is_pub: true,
1118            name: Ident::new(&spec.name),
1119            params,
1120            returns,
1121            body,
1122        }))
1123    }
1124
1125    /// A call to the generated defaulter, which is Table 9 once per
1126    /// type rather than once per position. Reached only inside a
1127    /// decoder, where `r.heap` is the arena a defaulted `@external`
1128    /// pointer allocates from.
1129    ///
1130    /// # Errors
1131    /// Never; the signature matches the emitters around it.
1132    fn default_into(
1133        ast: &Ast,
1134        id: Id<TypeNode>,
1135        place: Self::Expr,
1136    ) -> Result<Self::Stmt, EncodeError> {
1137        let defaulted = zig::Expr::call(
1138            &crate::default_zig::defaulter_name(ast, id),
1139            vec![Self::reader_heap()],
1140        );
1141        // The slot markers route exactly as [`Zig::read_call`]'s do.
1142        Ok(match place {
1143            ZigExpr::ExternalSlot(field) => zig::Stmt::Assign {
1144                place: field,
1145                value: zig::Expr::try_(zig::Expr::Call {
1146                    func: Box::new(Self::runtime_name("zdPtr")),
1147                    args: vec![Self::reader_heap(), zig::Expr::try_(defaulted)],
1148                }),
1149            },
1150            ZigExpr::OptionalSlot(field) | ZigExpr::Plain(field) | ZigExpr::Fallible(field) => {
1151                zig::Stmt::Assign {
1152                    place: field,
1153                    value: zig::Expr::try_(defaulted),
1154                }
1155            }
1156        })
1157    }
1158
1159    /// # Errors
1160    /// As [`crate::default_zig::member_default`]: a `@default` this
1161    /// column cannot spell is refused rather than silently replaced by
1162    /// Table 9's zero.
1163    fn member_default(
1164        ast: &Ast,
1165        node: &MemberNode,
1166        type_: Id<TypeNode>,
1167        place: Self::Expr,
1168    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1169        crate::default_zig::member_default(ast, node, type_, inner(place), Self::reader_heap())
1170    }
1171
1172    /// `Mode.IDLE` — the enum's own scope, which is where the
1173    /// materialiser declared the variant. The trait's default answers a
1174    /// bare prefixed name, which in Zig names nothing.
1175    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1176        plain(zig::Expr::dot(
1177            zig::Expr::Name(Ident::new(&zig_type_name(ast, id))),
1178            &field_name(enumerator),
1179        ))
1180    }
1181}
1182
1183// -- the assembly pass ---------------------------------------------------
1184
1185/// Every base name the body assigns through — `v` for `v.x = …` and
1186/// `v[i] = …` alike — which is what decides `var` against `const`.
1187fn assigned_roots(body: &[zig::Stmt]) -> Vec<String> {
1188    let mut out = Vec::new();
1189    collect_assigned(body, &mut out);
1190    out
1191}
1192
1193/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1194/// The tree walked here is the one this generator emitted and owns, so
1195/// it holds no cycle; the depth is that body's height, and a defaulter
1196/// body is a handful of statements deep.
1197fn collect_assigned(body: &[zig::Stmt], out: &mut Vec<String>) {
1198    for statement in body {
1199        match statement {
1200            zig::Stmt::Assign { place, .. } => {
1201                if let Some(root) = root_name(place) {
1202                    if !out.contains(&root) {
1203                        out.push(root);
1204                    }
1205                }
1206            }
1207            zig::Stmt::If {
1208                then, otherwise, ..
1209            } => {
1210                collect_assigned(then, out);
1211                if let Some(otherwise) = otherwise {
1212                    collect_assigned(otherwise, out);
1213                }
1214            }
1215            zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1216                collect_assigned(body, out);
1217            }
1218            _ => {}
1219        }
1220    }
1221}
1222
1223/// The base a place is reached from: `v` for `v.x[i].*`.
1224///
1225/// zerodds-lint: recursion-depth 64 (place-expression nesting the walk
1226/// builds). The tree walked here is the one this generator emitted and
1227/// owns, so it holds no cycle; the depth is that expression's height.
1228fn root_name(place: &zig::Expr) -> Option<String> {
1229    match place {
1230        zig::Expr::Name(name) => Some(name.as_str().to_string()),
1231        zig::Expr::Dot { value, .. }
1232        | zig::Expr::Index { value, .. }
1233        | zig::Expr::Paren(value)
1234        | zig::Expr::Try(value) => root_name(value),
1235        _ => None,
1236    }
1237}
1238
1239/// Turn every `var` the body never assigns into a `const`: Zig refuses
1240/// a `var` that is never mutated by name.
1241///
1242/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1243/// The tree walked here is the one this generator emitted and owns, so
1244/// it holds no cycle; the depth is that body's height, and a defaulter
1245/// body is a handful of statements deep.
1246fn fix_mutability(body: &mut [zig::Stmt], assigned: &[String]) {
1247    for statement in body {
1248        match statement {
1249            zig::Stmt::Var {
1250                name,
1251                value,
1252                mutable,
1253                ..
1254            } => {
1255                // A local declared `undefined` is assigned later by
1256                // definition; the scan sees those assignments too, but
1257                // keeping the rule explicit costs nothing.
1258                let is_assigned = assigned.iter().any(|root| root == name.as_str());
1259                *mutable = is_assigned || value.is_none();
1260            }
1261            zig::Stmt::If {
1262                then, otherwise, ..
1263            } => {
1264                fix_mutability(then, assigned);
1265                if let Some(otherwise) = otherwise {
1266                    fix_mutability(otherwise, assigned);
1267                }
1268            }
1269            zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1270                fix_mutability(body, assigned);
1271            }
1272            _ => {}
1273        }
1274    }
1275}
1276
1277/// Keep exactly the discards a body needs: drop `_ = x;` where `x` has
1278/// any other mention — Zig calls that a pointless discard — and add one
1279/// for every parameter the body never mentions, which Zig refuses as an
1280/// unused parameter.
1281fn settle_discards(body: &mut Vec<zig::Stmt>, params: &[zig::FnParam]) {
1282    let mut mentioned: Vec<String> = Vec::new();
1283    collect_mentions(body, &mut mentioned);
1284    prune_discards(body, &mentioned);
1285    let mut fronts = Vec::new();
1286    for param in params {
1287        let name = param.name.as_str();
1288        if !mentioned.iter().any(|m| m == name) {
1289            fronts.push(zig::Stmt::Discard(Ident::new(name)));
1290        }
1291    }
1292    if !fronts.is_empty() {
1293        fronts.append(body);
1294        *body = fronts;
1295    }
1296}
1297
1298/// Every name the body mentions outside a discard — in an expression,
1299/// as an assignment target, as a loop bound.
1300///
1301/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1302/// The tree walked here is the one this generator emitted and owns, so
1303/// it holds no cycle; the depth is that body's height, and a defaulter
1304/// body is a handful of statements deep.
1305fn collect_mentions(body: &[zig::Stmt], out: &mut Vec<String>) {
1306    for statement in body {
1307        match statement {
1308            zig::Stmt::Var { value, .. } => {
1309                if let Some(value) = value {
1310                    mentions_in_expr(value, out);
1311                }
1312            }
1313            zig::Stmt::Assign { place, value } => {
1314                mentions_in_expr(place, out);
1315                mentions_in_expr(value, out);
1316            }
1317            zig::Stmt::Expr(value)
1318            | zig::Stmt::DiscardExpr(value)
1319            | zig::Stmt::Return(Some(value)) => {
1320                mentions_in_expr(value, out);
1321            }
1322            zig::Stmt::If {
1323                condition,
1324                then,
1325                otherwise,
1326            } => {
1327                mentions_in_expr(condition, out);
1328                collect_mentions(then, out);
1329                if let Some(otherwise) = otherwise {
1330                    collect_mentions(otherwise, out);
1331                }
1332            }
1333            zig::Stmt::CountUp { bound, body, .. } => {
1334                mentions_in_expr(bound, out);
1335                collect_mentions(body, out);
1336            }
1337            zig::Stmt::While { condition, body } => {
1338                mentions_in_expr(condition, out);
1339                collect_mentions(body, out);
1340            }
1341            zig::Stmt::Discard(_)
1342            | zig::Stmt::Return(None)
1343            | zig::Stmt::Break
1344            | zig::Stmt::Comment(_) => {}
1345        }
1346    }
1347}
1348
1349/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
1350/// The tree walked here is the one this generator emitted and owns, so
1351/// it holds no cycle; the depth is that expression's height, which
1352/// follows the member nesting the type walk already bounded.
1353fn mentions_in_expr(expr: &zig::Expr, out: &mut Vec<String>) {
1354    match expr {
1355        zig::Expr::Name(name) => {
1356            let text = name.as_str().to_string();
1357            if !out.contains(&text) {
1358                out.push(text);
1359            }
1360        }
1361        zig::Expr::Binary { lhs, rhs, .. } => {
1362            mentions_in_expr(lhs, out);
1363            mentions_in_expr(rhs, out);
1364        }
1365        zig::Expr::Paren(value)
1366        | zig::Expr::Unary { value, .. }
1367        | zig::Expr::Try(value)
1368        | zig::Expr::Dot { value, .. } => mentions_in_expr(value, out),
1369        zig::Expr::Index { value, index } => {
1370            mentions_in_expr(value, out);
1371            mentions_in_expr(index, out);
1372        }
1373        zig::Expr::Call { func, args } => {
1374            mentions_in_expr(func, out);
1375            for arg in args {
1376                mentions_in_expr(arg, out);
1377            }
1378        }
1379        zig::Expr::StructLit(fields) => {
1380            for (_, value) in fields {
1381                mentions_in_expr(value, out);
1382            }
1383        }
1384        zig::Expr::ListLit(elements) => {
1385            for element in elements {
1386                mentions_in_expr(element, out);
1387            }
1388        }
1389        zig::Expr::Literal(_) | zig::Expr::Type(_) => {}
1390    }
1391}
1392
1393/// Drop every `_ = x;` whose `x` the body mentions anywhere else.
1394///
1395/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
1396/// The tree walked here is the one this generator emitted and owns, so
1397/// it holds no cycle; the depth is that body's height, and a defaulter
1398/// body is a handful of statements deep.
1399fn prune_discards(body: &mut Vec<zig::Stmt>, mentioned: &[String]) {
1400    body.retain(|statement| match statement {
1401        zig::Stmt::Discard(name) => !mentioned.iter().any(|m| m == name.as_str()),
1402        _ => true,
1403    });
1404    for statement in body {
1405        match statement {
1406            zig::Stmt::If {
1407                then, otherwise, ..
1408            } => {
1409                prune_discards(then, mentioned);
1410                if let Some(otherwise) = otherwise {
1411                    prune_discards(otherwise, mentioned);
1412                }
1413            }
1414            zig::Stmt::CountUp { body, .. } | zig::Stmt::While { body, .. } => {
1415                prune_discards(body, mentioned);
1416            }
1417            _ => {}
1418        }
1419    }
1420}
1421
1422/// The bound a collection carries, as the suffix that keeps two of them
1423/// apart.
1424///
1425/// The tag names the routine a hoisted collection's codec is emitted
1426/// under, and `decoder_over` emits one body per name. Without the
1427/// bound, `sequence<long>` and `sequence<long, 4>` are one name and the
1428/// first body wins: measured on
1429/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1430/// generated Zig held no `zd_over` check at all, so `a` accepted a
1431/// stream of any length; with the members the other way round both
1432/// members got the bounded body, so `b` refused — and truncated with
1433/// `v = v[0 .. 4]` — a length the IDL puts no limit on. `string` beside
1434/// `string<4>` behaved the same way through `trimUtf8`. Either way the
1435/// reader answered with a value the IDL does not describe and said
1436/// nothing, which is why the bound is part of the name.
1437fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1438    match bound.expr() {
1439        None => String::new(),
1440        // `_b` and not a bare `_n`: `seq_long_4` would be an array of
1441        // four `long` under the arm above, and the two are different
1442        // wire forms.
1443        Some(expr) => match ast.eval_integral(expr) {
1444            Ok(limit) => format!("_b{limit}"),
1445            // As the array's `_x`: a bound that does not evaluate is
1446            // still not the unbounded form, and the materialiser
1447            // reports the expression that would not evaluate.
1448            Err(_) => "_bx".to_string(),
1449        },
1450    }
1451}
1452
1453/// The name a codec function takes for the type it codes: the IDL kind
1454/// and not the Zig spelling, structurally for a collection and the
1455/// flattened Zig name for a declaration — the same scheme the D and Go
1456/// columns carry, for the reasons their `type_tag`s state.
1457///
1458/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1459#[must_use]
1460pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
1461    if depth > MAX_TYPE_DEPTH {
1462        return "unresolved".to_string();
1463    }
1464    let node = match ast.type_(id) {
1465        Ok(node) => node,
1466        Err(_) => return "unresolved".to_string(),
1467    };
1468    match &node.kind {
1469        TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1470        TypeKind::String {
1471            width: CharWidth::Char,
1472            bound,
1473        } => format!("string{}", bound_tag(ast, bound)),
1474        TypeKind::String {
1475            width: CharWidth::WChar,
1476            bound,
1477        } => format!("wstring{}", bound_tag(ast, bound)),
1478        TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1479        TypeKind::Sequence { element, bound } => match element.resolved() {
1480            Some(element) => format!(
1481                "seq{}_{}",
1482                bound_tag(ast, bound),
1483                type_tag(ast, element, depth + 1)
1484            ),
1485            None => format!("seq_unresolved{}", bound_tag(ast, bound)),
1486        },
1487        TypeKind::Array { element, dims } => {
1488            let mut name = match element.resolved() {
1489                Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)),
1490                None => "arr_unresolved".to_string(),
1491            };
1492            for dim in dims {
1493                match ast.eval_integral(*dim) {
1494                    Ok(length) => name.push_str(&format!("_{length}")),
1495                    // No number rather than a zero: the name has to
1496                    // differ from a `[0]` array's, and the materialiser
1497                    // reports the dimension that would not evaluate.
1498                    Err(_) => name.push_str("_x"),
1499                }
1500            }
1501            name
1502        }
1503        TypeKind::Map { key, value, bound } => {
1504            let half = |half: Option<Id<TypeNode>>| match half {
1505                Some(target) => type_tag(ast, target, depth + 1),
1506                None => "unresolved".to_string(),
1507            };
1508            format!(
1509                "map{}_{}_{}",
1510                bound_tag(ast, bound),
1511                half(key.resolved()),
1512                half(value.resolved())
1513            )
1514        }
1515        // An alias is followed through: two IDL names for one shape are
1516        // one wire form, and two codecs for it would be two chances to
1517        // differ.
1518        TypeKind::Alias { aliased } => match aliased.resolved() {
1519            Some(target) => type_tag(ast, target, depth + 1),
1520            None => "unresolved".to_string(),
1521        },
1522        TypeKind::ValueBox { boxed } => match boxed.resolved() {
1523            Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)),
1524            None => "box_unresolved".to_string(),
1525        },
1526        TypeKind::Structure { .. }
1527        | TypeKind::Union { .. }
1528        | TypeKind::Enumeration { .. }
1529        | TypeKind::Bitmask { .. }
1530        | TypeKind::Bitset { .. }
1531        | TypeKind::Valuetype { .. }
1532        | TypeKind::Native => zig_type_name(ast, id),
1533        TypeKind::Any => "any".to_string(),
1534        TypeKind::ValueBase => "valuebase".to_string(),
1535        TypeKind::ObjectRef { .. } => "objectref".to_string(),
1536        TypeKind::AnnotationType { .. } => "annotation".to_string(),
1537    }
1538}