Skip to main content

zerodds_lang_rules/emit/
nim.rs

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