Skip to main content

zerodds_lang_rules/
default_zig.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value a Zig member takes when the stream does not carry it.
4//!
5//! Table 9 of DDS-XTypes 1.3, as Zig — deliberately a further
6//! derivation and not a call into any sibling's, because the columns
7//! are compared against each other and a shared helper would make them
8//! agree about a mistake.
9//!
10//! ## The shape is the D column's, with two Zig differences
11//!
12//! A defaulter **returns** Table 9's value and the caller assigns it;
13//! `emit/zig.rs`'s decoder opens with `var v: T = try zd_def_T(r.heap);`.
14//!
15//! **An aggregate's body is one struct literal**, not a declaration and
16//! assignments: Zig has no default-initialised `T v;` — a local is
17//! either fully written or `undefined` — so the literal is where every
18//! member's value is stated. An `@optional` member is left out of it:
19//! the field carries `= null` in the type declaration, and absence is
20//! §7.2.2.4.4.4.8's default state. A union's unselected branches take
21//! `undefined`: no read reaches one — the encoder writes the selected
22//! branch, the dumper prints it — and a defaulter call there would
23//! recurse forever on a self-referential `@external` branch.
24//!
25//! **A defaulter takes an allocator.** A non-optional `@external`
26//! member is Table 9's value behind the column's indirection —
27//! `try zdwire.zdPtr(zd_al, try zd_def_T(zd_al))` — and Zig has no
28//! garbage collector to hide the allocation behind. The decoder hands
29//! its reader's arena; a defaulter for a type with no such member
30//! simply discards the parameter.
31
32use crate::adapter::zig::{field_name, zig_type_name, zig_value_type};
33use crate::default_common::{discriminator_default_value, resolve_alias, selected_case};
34use crate::emit::zig::type_tag;
35use crate::encode::EncodeError;
36use crate::fixed_literal::{fixed_text, zero_decimal};
37use zerodds_ast::{
38    Ast, ConstExpr, Literal as AstLiteral, MemberNode, PrimitiveKind, TypeKind, TypeNode,
39};
40use zerodds_ast_core::Id;
41use zerodds_lang_cst::common::{Doc, Ident, Literal};
42use zerodds_lang_cst::zig;
43
44/// How deep a chain of typedefs is followed before the model is taken
45/// to be malformed.
46const MAX_ALIAS_DEPTH: usize = 64;
47
48/// The name of the allocator parameter every defaulter takes.
49const ALLOCATOR: &str = "zd_al";
50
51/// The defaulter function name for a type, keyed the way the decoder
52/// is: by the IDL kind for a primitive and by the codec tag for
53/// everything else.
54#[must_use]
55pub fn defaulter_name(ast: &Ast, id: Id<TypeNode>) -> String {
56    match ast.type_(id).map(|node| node.kind.clone()) {
57        Ok(TypeKind::Primitive(kind)) => {
58            format!("zd_def_p_{}", kind.as_str().replace(' ', "_"))
59        }
60        _ => format!("zd_def_{}", type_tag(ast, id, 0)),
61    }
62}
63
64/// The defaulter that goes with a decoder, by name.
65#[must_use]
66pub fn defaulter_from_decoder(decoder: &str) -> String {
67    match decoder.strip_prefix("zd_dec_") {
68        Some(rest) => format!("zd_def_{rest}"),
69        None => format!("zd_def_{decoder}"),
70    }
71}
72
73/// One member's default — its `@default`, or its type's — assigned into
74/// `place`. §7.2.2.4.4.4.9 puts the explicit one first. An `@external`
75/// member's value goes through `zdwire.zdPtr` with `alloc`; an
76/// `@optional` one takes the bare value, which Zig coerces into the
77/// `?T` on its own.
78///
79/// # Errors
80/// [`EncodeError::NoWriter`] where a `@default` names a value this
81/// column cannot spell. Refused rather than silently replaced by
82/// Table 9's zero: the two are different samples.
83pub fn member_default(
84    ast: &Ast,
85    node: &MemberNode,
86    type_: Id<TypeNode>,
87    place: zig::Expr,
88    alloc: zig::Expr,
89) -> Result<Vec<zig::Stmt>, EncodeError> {
90    let value = member_value(ast, node, type_, &alloc)?;
91    let external = crate::adapter::held_externally(ast, node);
92    let value = if external { boxed(alloc, value) } else { value };
93    Ok(vec![zig::Stmt::Assign { place, value }])
94}
95
96/// The defaulters `decls` call, and exactly those — read off the
97/// emitted calls rather than derived a second time from the model.
98///
99/// # Errors
100/// [`EncodeError`] for a construct with no Zig default.
101pub fn zig_defaulters_called_by(
102    ast: &Ast,
103    decls: &[zig::Decl],
104) -> Result<Vec<zig::Decl>, EncodeError> {
105    let index: Vec<(String, Id<TypeNode>)> = ast
106        .types()
107        .map(|(id, _)| (defaulter_name(ast, id), id))
108        .collect();
109    let mut called: Vec<String> = Vec::new();
110    for decl in decls {
111        calls_in_decl(decl, &mut called);
112    }
113    let mut wanted: Vec<Id<TypeNode>> = Vec::new();
114    let mut have: Vec<String> = Vec::new();
115    for _ in 0..=index.len() {
116        let mut grew = false;
117        for name in &called {
118            if have.contains(name) {
119                continue;
120            }
121            have.push(name.clone());
122            // A name with no type behind it cannot happen for a call
123            // this generator emitted; if it ever does, the omission is
124            // an undefined-identifier error from the Zig compiler
125            // naming the function.
126            if let Some(id) = index.iter().find(|(n, _)| n == name).map(|(_, id)| *id) {
127                wanted.push(id);
128                grew = true;
129            }
130        }
131        let produced = zig_defaulters_over(ast, &wanted)?;
132        if !grew {
133            return Ok(produced);
134        }
135        for decl in &produced {
136            calls_in_decl(decl, &mut called);
137        }
138    }
139    zig_defaulters_over(ast, &wanted)
140}
141
142/// Build the defaulter for each of `types`, once per name.
143///
144/// # Errors
145/// [`EncodeError`] for a construct with no Zig default.
146pub fn zig_defaulters_over(
147    ast: &Ast,
148    types: &[Id<TypeNode>],
149) -> Result<Vec<zig::Decl>, EncodeError> {
150    let mut out = Vec::new();
151    let mut emitted: Vec<String> = Vec::new();
152    for id in types.iter().copied() {
153        let name = defaulter_name(ast, id);
154        if emitted.contains(&name) {
155            continue;
156        }
157        let body = match body_of(ast, id, &name)? {
158            Some(body) => body,
159            None => continue,
160        };
161        let returns = match zig_value_type(ast, resolve_alias(ast, id), &name) {
162            Ok(returns) => returns,
163            Err(_) => {
164                return Err(EncodeError::NoWriter {
165                    member: name,
166                    kind: "type with no Zig form to return",
167                });
168            }
169        };
170        emitted.push(name.clone());
171        let mut body = body;
172        // The allocator is only reached where an `@external` member or
173        // a nested defaulter needs one; Zig refuses an unused
174        // parameter, and the discard is its way of saying the non-use
175        // is meant.
176        if !body_mentions(&body, ALLOCATOR) {
177            body.insert(0, zig::Stmt::Discard(Ident::new(ALLOCATOR)));
178        }
179        out.push(zig::Decl::Func(zig::FuncDecl {
180            doc: Doc::line(
181                "Table 9 of DDS-XTypes 1.3: what this type is where the stream did not \
182                 carry it.",
183            ),
184            is_pub: false,
185            name: Ident::new(&name),
186            params: vec![zig::FnParam {
187                name: Ident::new(ALLOCATOR),
188                type_: zig::Type::named("std.mem.Allocator"),
189            }],
190            returns: zig::Type::error_union(zig::Type::named("zdwire.WireError"), returns),
191            body,
192        }));
193    }
194    Ok(out)
195}
196
197/// Whether any statement of `body` names `name` in an expression.
198fn body_mentions(body: &[zig::Stmt], name: &str) -> bool {
199    /// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
200    /// The tree walked here is the one this generator emitted and owns, so
201    /// it holds no cycle; the depth is that expression's height, which
202    /// follows the member nesting the type walk already bounded.
203    fn in_expr(expr: &zig::Expr, name: &str) -> bool {
204        match expr {
205            zig::Expr::Name(ident) => ident.as_str() == name,
206            zig::Expr::Binary { lhs, rhs, .. } => in_expr(lhs, name) || in_expr(rhs, name),
207            zig::Expr::Paren(value)
208            | zig::Expr::Unary { value, .. }
209            | zig::Expr::Try(value)
210            | zig::Expr::Dot { value, .. } => in_expr(value, name),
211            zig::Expr::Index { value, index } => in_expr(value, name) || in_expr(index, name),
212            zig::Expr::Call { func, args } => {
213                in_expr(func, name) || args.iter().any(|arg| in_expr(arg, name))
214            }
215            zig::Expr::StructLit(fields) => fields.iter().any(|(_, value)| in_expr(value, name)),
216            zig::Expr::ListLit(elements) => elements.iter().any(|element| in_expr(element, name)),
217            zig::Expr::Literal(_) | zig::Expr::Type(_) => false,
218        }
219    }
220    body.iter().any(|statement| match statement {
221        zig::Stmt::Return(Some(value)) | zig::Stmt::Expr(value) | zig::Stmt::DiscardExpr(value) => {
222            in_expr(value, name)
223        }
224        zig::Stmt::Assign { place, value } => in_expr(place, name) || in_expr(value, name),
225        zig::Stmt::Var { value, .. } => value.as_ref().is_some_and(|value| in_expr(value, name)),
226        _ => false,
227    })
228}
229
230/// The body one defaulter answers with, or `None` where the type has no
231/// Zig data form at all.
232#[allow(clippy::too_many_lines)]
233fn body_of(
234    ast: &Ast,
235    id: Id<TypeNode>,
236    own_name: &str,
237) -> Result<Option<Vec<zig::Stmt>>, EncodeError> {
238    // Before the table and not as one of its rows: a typedef and its
239    // target share a defaulter name.
240    let id = resolve_alias(ast, id);
241    let refuse = |kind: &'static str| EncodeError::NoWriter {
242        member: own_name.to_string(),
243        kind,
244    };
245    // Refused and not `Ok(None)`. `None` is this function's word for
246    // "the type has no Zig data form", and the caller reads it as a
247    // reason to emit no defaulter and walk on. A handle the model will
248    // not answer for is not that: the type does have a form, and the
249    // defaulter the closure was asked for goes missing from the batch
250    // while the generated decoder still calls it by name.
251    // Refused and not `Ok(None)`. `None` is this function's word for
252    // "the type has no Zig data form", and the caller reads it as a
253    // reason to emit no defaulter and walk on. A handle the model will
254    // not answer for is not that: the type does have a form, and the
255    // defaulter the closure was asked for goes missing from the batch
256    // while the generated decoder still calls it by name.
257    let Ok(node) = ast.type_(id) else {
258        return Err(refuse("a type the model does not hold"));
259    };
260    let give = |value: zig::Expr| Some(vec![zig::Stmt::Return(Some(value))]);
261    let alloc = || zig::Expr::Name(Ident::new(ALLOCATOR));
262    let body = match &node.kind {
263        TypeKind::Primitive(kind) => give(primitive_default(*kind)),
264
265        // `""`, for both widths: the member holds UTF-8 either way.
266        TypeKind::String { .. } => give(text("")),
267
268        // The one decimal spelling of zero for the scale.
269        TypeKind::Fixed { scale, .. } => give(text(&zero_decimal(*scale))),
270
271        TypeKind::Alias { .. } => {
272            return Err(refuse("typedef with an unresolved target"));
273        }
274
275        // "The first literal in the enumeration", or the one
276        // `@default_literal` names — §7.3.1.2.1.10 — by name, in the
277        // enum's own scope: `Mode.IDLE`.
278        TypeKind::Enumeration { .. } => {
279            let enumerator = match crate::emit::default_enumerator_name(ast, id) {
280                Some(enumerator) => enumerator,
281                None => {
282                    return Err(refuse(
283                        "enumeration with no enumerator to take as its default",
284                    ));
285                }
286            };
287            give(enumerator_expr(ast, id, &enumerator))
288        }
289
290        // No bit set; the alias's underlying unsigned takes a plain
291        // zero.
292        TypeKind::Bitmask { .. } => give(zig::Expr::int(0)),
293
294        // A bitset is a struct of separate holders, every one zero.
295        TypeKind::Bitset { .. } => {
296            let mut fields = Vec::new();
297            for field in &ast.all_bitfields_of(id) {
298                let Ok(field_node) = ast.bitfield(*field) else {
299                    continue;
300                };
301                let Some(field_ident) = field_node.name.as_ref() else {
302                    continue;
303                };
304                fields.push((Ident::new(&field_name(field_ident)), zig::Expr::int(0)));
305            }
306            give(zig::Expr::StructLit(fields))
307        }
308
309        // "A zero-length sequence of the same element type", and the
310        // same for a map's entries: the empty slice, typed by the
311        // return.
312        TypeKind::Sequence { .. } | TypeKind::Map { .. } => give(zig::Expr::Unary {
313            op: "&",
314            value: Box::new(zig::Expr::ListLit(Vec::new())),
315        }),
316
317        // "An array of the same dimensions and same element type whose
318        // elements take the default value for their corresponding
319        // type." One call per element, nested list literals, outermost
320        // dimension outermost; the return type gives every level its
321        // shape.
322        TypeKind::Array { element, dims } => {
323            let element = match element.resolved() {
324                Some(element) => element,
325                None => return Err(refuse("array with an unresolved element")),
326            };
327            let mut lengths = Vec::new();
328            for dim in dims {
329                let length = match ast.eval_integral(*dim) {
330                    Ok(length) => length,
331                    Err(_) => return Err(refuse("array whose length does not evaluate")),
332                };
333                let length = match usize::try_from(length) {
334                    Ok(length) => length,
335                    Err(_) => return Err(refuse("array of a length this column cannot spell")),
336                };
337                lengths.push(length);
338            }
339            let mut out = zig::Expr::try_(zig::Expr::call(
340                &defaulter_name(ast, element),
341                vec![alloc()],
342            ));
343            for length in lengths.into_iter().rev() {
344                out = zig::Expr::ListLit(
345                    core::iter::repeat_with(|| out.clone())
346                        .take(length)
347                        .collect(),
348                );
349            }
350            give(out)
351        }
352
353        // "A structure without any of the optional members and with
354        // other members set to their default values." The literal
355        // states the values; an `@optional` member is left out and its
356        // declared `= null` default is the absence.
357        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
358            let mut fields = Vec::new();
359            // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
360            if let Some(base) = crate::adapter::c::struct_base(ast, id) {
361                fields.push((
362                    Ident::new(&field_name("parent")),
363                    zig::Expr::try_(zig::Expr::call(&defaulter_name(ast, base), vec![alloc()])),
364                ));
365            }
366            for member_id in ast.members_of(id) {
367                let member_node = match ast.member(*member_id) {
368                    Ok(member_node) => member_node,
369                    Err(_) => continue,
370                };
371                if member_node.facets.is_optional() {
372                    continue;
373                }
374                let member_type = match member_node.type_.resolved() {
375                    Some(member_type) => member_type,
376                    None => {
377                        return Err(EncodeError::NoWriter {
378                            member: member_node.name.clone(),
379                            kind: "member with an unresolved type",
380                        });
381                    }
382                };
383                let mut value = member_value(ast, member_node, member_type, &alloc())?;
384                let external = crate::adapter::held_externally(ast, member_node);
385                if external {
386                    value = boxed(alloc(), value);
387                }
388                fields.push((Ident::new(&field_name(&member_node.name)), value));
389            }
390            give(zig::Expr::StructLit(fields))
391        }
392
393        // "A union with the discriminator set to the default value for
394        // the discriminator type. If this selects a branch, then the
395        // selected member is also set to the default value for the
396        // member type." The unselected branches take `undefined`: no
397        // read reaches one, and the module documentation carries why a
398        // defaulter call there would be worse.
399        TypeKind::Union { discriminator, .. } => {
400            let discriminator = match discriminator.resolved() {
401                Some(discriminator) => discriminator,
402                None => return Err(refuse("union with an unresolved discriminator")),
403            };
404            let selector = discriminator_default_value(ast, discriminator);
405            let selected = selected_by_default(ast, id, selector)?;
406            let mut fields = vec![(
407                Ident::new("_d"),
408                discriminant_literal(ast, discriminator, selector.unwrap_or(0)),
409            )];
410            for case in ast.cases_of(id) {
411                let Ok(case_node) = ast.case(*case) else {
412                    return Err(refuse("a case the model does not hold"));
413                };
414                let Ok(member) = ast.member(case_node.member) else {
415                    return Err(refuse("a member the model does not hold"));
416                };
417                let field = field_name(&member.name);
418                let value = match &selected {
419                    Some((member_type, selected_field)) if *selected_field == field => {
420                        zig::Expr::try_(zig::Expr::call(
421                            &defaulter_name(ast, *member_type),
422                            vec![alloc()],
423                        ))
424                    }
425                    _ => zig::Expr::Name(Ident::new("undefined")),
426                };
427                fields.push((Ident::new(&field), value));
428            }
429            give(zig::Expr::StructLit(fields))
430        }
431
432        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => None,
433        TypeKind::ValueBox { .. } => {
434            return Err(refuse(
435                "boxed value, whose presence flag has no settled wire form",
436            ));
437        }
438        TypeKind::Any => {
439            return Err(refuse(
440                "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
441                 as a TypeCode ahead of the value",
442            ));
443        }
444        TypeKind::ValueBase => {
445            return Err(refuse(
446                "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
447                 one, as a chunked value with its repository id",
448            ));
449        }
450    };
451    Ok(body)
452}
453
454/// `try zdwire.zdPtr(alloc, value)` — Table 9's value behind the
455/// column's `@external` indirection.
456fn boxed(alloc: zig::Expr, value: zig::Expr) -> zig::Expr {
457    zig::Expr::try_(zig::Expr::Call {
458        func: Box::new(zig::Expr::dot(
459            zig::Expr::Name(Ident::new("zdwire")),
460            "zdPtr",
461        )),
462        args: vec![alloc, value],
463    })
464}
465
466/// One member's default as a value: its `@default`, or a call to its
467/// type's defaulter.
468fn member_value(
469    ast: &Ast,
470    node: &MemberNode,
471    type_: Id<TypeNode>,
472    alloc: &zig::Expr,
473) -> Result<zig::Expr, EncodeError> {
474    match node.facets.xtypes.as_ref().and_then(|x| x.default_value) {
475        Some(expr) => explicit(ast, expr, type_, &node.name),
476        None => Ok(zig::Expr::try_(zig::Expr::call(
477            &defaulter_name(ast, type_),
478            vec![alloc.clone()],
479        ))),
480    }
481}
482
483/// Table 9's primitive rows, as the Zig holder each takes. The integers
484/// and both character widths are integer zeros — the member holds the
485/// code unit — and the three float widths are `0.0`, the binary128
486/// included: this column's `f128` is real.
487fn primitive_default(kind: PrimitiveKind) -> zig::Expr {
488    match kind {
489        PrimitiveKind::Boolean => zig::Expr::Literal(Literal::Bool(false)),
490        PrimitiveKind::Float32 | PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
491            zig::Expr::Literal(Literal::Float("0.0".to_string()))
492        }
493        PrimitiveKind::Char
494        | PrimitiveKind::WChar
495        | PrimitiveKind::Octet
496        | PrimitiveKind::Int8
497        | PrimitiveKind::UInt8
498        | PrimitiveKind::Int16
499        | PrimitiveKind::UInt16
500        | PrimitiveKind::Int32
501        | PrimitiveKind::UInt32
502        | PrimitiveKind::Int64
503        | PrimitiveKind::UInt64 => zig::Expr::int(0),
504    }
505}
506
507/// An enumerator, in the enum's own scope: `Mode.IDLE`.
508fn enumerator_expr(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> zig::Expr {
509    zig::Expr::dot(
510        zig::Expr::Name(Ident::new(&zig_type_name(ast, id))),
511        &field_name(enumerator),
512    )
513}
514
515/// The case a defaulted discriminator selects, if any.
516fn selected_by_default(
517    ast: &Ast,
518    id: Id<TypeNode>,
519    selector: Option<i128>,
520) -> Result<Option<(Id<TypeNode>, String)>, EncodeError> {
521    let selector = match selector {
522        Some(selector) => selector,
523        None => return Ok(None),
524    };
525    Ok(selected_case(ast, id, selector)?
526        .map(|(member, member_type)| (member_type, field_name(&member.name))))
527}
528
529/// A discriminator value in the Zig form its type gives it: the
530/// enumerator for an enumeration, `true`/`false` for a `boolean`, and
531/// the number for everything else — a `char` and a `wchar`
532/// discriminator hold their code unit as an integer here.
533fn discriminant_literal(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> zig::Expr {
534    let resolved = resolve_alias(ast, discriminator);
535    if matches!(
536        ast.type_(resolved).map(|node| node.kind.clone()),
537        Ok(TypeKind::Enumeration { .. })
538    ) {
539        if let Some(enumerator) = crate::emit::default_enumerator_name(ast, resolved) {
540            return enumerator_expr(ast, resolved, &enumerator);
541        }
542    }
543    match resolved_primitive(ast, resolved) {
544        Some(PrimitiveKind::Boolean) => zig::Expr::Literal(Literal::Bool(value != 0)),
545        _ => zig::Expr::int(value),
546    }
547}
548
549/// The primitive `id` is once typedefs are seen through, or `None`.
550fn resolved_primitive(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
551    let mut current = id;
552    for _ in 0..MAX_ALIAS_DEPTH {
553        match ast.type_(current).map(|node| node.kind.clone()) {
554            Ok(TypeKind::Primitive(kind)) => return Some(kind),
555            Ok(TypeKind::Alias { aliased }) => current = aliased.resolved()?,
556            _ => return None,
557        }
558    }
559    None
560}
561
562/// An explicit `@default` as a Zig expression for `type_`.
563#[allow(clippy::too_many_lines)]
564fn explicit(
565    ast: &Ast,
566    expr: Id<zerodds_ast::ConstExprNode>,
567    type_: Id<TypeNode>,
568    member_name: &str,
569) -> Result<zig::Expr, EncodeError> {
570    let refuse = |kind: &'static str| EncodeError::NoWriter {
571        member: member_name.to_string(),
572        kind,
573    };
574    let node = match ast.expr(expr) {
575        Ok(node) => node,
576        Err(_) => return Err(refuse("a `@default` this frontend cannot follow")),
577    };
578    let resolved = resolve_alias(ast, type_);
579    let kind = ast.type_(resolved).map(|node| node.kind.clone());
580    match &node.expr {
581        ConstExpr::Literal(AstLiteral::Boolean(flag)) => {
582            Ok(zig::Expr::Literal(Literal::Bool(*flag)))
583        }
584        // The code unit as the number the member holds: `u8` for a
585        // `char`, `u16` for a `wchar`.
586        ConstExpr::Literal(AstLiteral::Char(ch)) => Ok(zig::Expr::int(i128::from(u32::from(*ch)))),
587        ConstExpr::Literal(AstLiteral::WChar(code)) => Ok(zig::Expr::int(i128::from(*code))),
588        ConstExpr::Literal(AstLiteral::String(value)) => Ok(text(value)),
589        ConstExpr::Literal(AstLiteral::WString(codes)) => {
590            let mut out = String::new();
591            for code in codes {
592                let ch = match char::from_u32(*code) {
593                    Some(ch) => ch,
594                    None => {
595                        return Err(refuse("a `@default` wide string that is not code points"));
596                    }
597                };
598                out.push(ch);
599            }
600            Ok(text(&out))
601        }
602        // The decimal, as a literal: Zig's `comptime_float` is
603        // evaluated at `f128` width, so the number in the file is the
604        // number the IDL wrote — at every one of the three widths.
605        ConstExpr::Literal(AstLiteral::Float { mantissa, exponent }) => {
606            let exponent = match exponent.as_ref() {
607                Some(exponent) => format!("e{exponent}"),
608                None => String::new(),
609            };
610            Ok(zig::Expr::Literal(Literal::Float(format!(
611                "{mantissa}{exponent}"
612            ))))
613        }
614        ConstExpr::Literal(AstLiteral::Fixed { digits, scale }) => {
615            Ok(text(&fixed_text(digits, *scale)))
616        }
617        ConstExpr::Literal(AstLiteral::Enumerator(reference)) => {
618            let target = match reference.resolved() {
619                Some(target) => target,
620                None => return Err(refuse("a `@default` enumerator that is unresolved")),
621            };
622            let enumerator = match ast.enumerator(target) {
623                Ok(enumerator) => enumerator,
624                Err(_) => {
625                    return Err(refuse(
626                        "a `@default` enumerator this frontend cannot follow",
627                    ));
628                }
629            };
630            Ok(enumerator_expr(ast, resolved, &enumerator.name))
631        }
632        _ => {
633            let value = match ast.eval_integral(expr) {
634                Ok(value) => value,
635                Err(_) => return Err(refuse("a `@default` this frontend cannot evaluate")),
636            };
637            Ok(match kind {
638                Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
639                    zig::Expr::Literal(Literal::Bool(value != 0))
640                }
641                Ok(TypeKind::Primitive(PrimitiveKind::Char)) => {
642                    if !(0..=0xFF).contains(&value) {
643                        return Err(refuse("a `@default` character outside one octet"));
644                    }
645                    zig::Expr::int(value)
646                }
647                Ok(TypeKind::Primitive(PrimitiveKind::WChar)) => {
648                    if !(0..=0xFFFF).contains(&value) {
649                        return Err(refuse(
650                            "a `@default` character outside the range of a UTF-16 code unit",
651                        ));
652                    }
653                    zig::Expr::int(value)
654                }
655                _ => zig::Expr::int(value),
656            })
657        }
658    }
659}
660
661// -- reading the calls back out ----------------------------------------
662
663/// The `zd_def_*` functions one declaration calls, appended to `out`.
664fn calls_in_decl(decl: &zig::Decl, out: &mut Vec<String>) {
665    match decl {
666        zig::Decl::Func(func) => calls_in_body(&func.body, out),
667        zig::Decl::Const(constant) => calls_in_expr(&constant.value, out),
668        zig::Decl::Comment(_) | zig::Decl::Verbatim(_) | zig::Decl::AnalyseAll(_) => {}
669    }
670}
671
672/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
673/// The tree walked here is the one this generator emitted and owns, so
674/// it holds no cycle; the depth is that body's height, and a defaulter
675/// body is a handful of statements deep.
676fn calls_in_body(body: &[zig::Stmt], out: &mut Vec<String>) {
677    for statement in body {
678        calls_in_stmt(statement, out);
679    }
680}
681
682/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
683/// The tree walked here is the one this generator emitted and owns, so
684/// it holds no cycle; the depth is that body's height, and a defaulter
685/// body is a handful of statements deep.
686fn calls_in_stmt(statement: &zig::Stmt, out: &mut Vec<String>) {
687    match statement {
688        zig::Stmt::Var { value, .. } => {
689            if let Some(value) = value {
690                calls_in_expr(value, out);
691            }
692        }
693        zig::Stmt::Assign { place, value } => {
694            calls_in_expr(place, out);
695            calls_in_expr(value, out);
696        }
697        zig::Stmt::Expr(value) | zig::Stmt::DiscardExpr(value) => calls_in_expr(value, out),
698        zig::Stmt::Return(value) => {
699            if let Some(value) = value {
700                calls_in_expr(value, out);
701            }
702        }
703        zig::Stmt::If {
704            condition,
705            then,
706            otherwise,
707        } => {
708            calls_in_expr(condition, out);
709            calls_in_body(then, out);
710            if let Some(otherwise) = otherwise {
711                calls_in_body(otherwise, out);
712            }
713        }
714        zig::Stmt::CountUp { bound, body, .. } => {
715            calls_in_expr(bound, out);
716            calls_in_body(body, out);
717        }
718        zig::Stmt::While { condition, body } => {
719            calls_in_expr(condition, out);
720            calls_in_body(body, out);
721        }
722        zig::Stmt::Discard(_) | zig::Stmt::Break | zig::Stmt::Comment(_) => {}
723    }
724}
725
726/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
727/// The tree walked here is the one this generator emitted and owns, so
728/// it holds no cycle; the depth is that expression's height, which
729/// follows the member nesting the type walk already bounded.
730fn calls_in_expr(expr: &zig::Expr, out: &mut Vec<String>) {
731    match expr {
732        zig::Expr::Call { func, args } => {
733            // Unqualified only: a defaulter is a file-level function of
734            // the generated program, and this generator built every
735            // call to one with `defaulter_name`; `zdwire.zdPtr(..)` is
736            // the runtime's and never a defaulter.
737            if let zig::Expr::Name(name) = func.as_ref() {
738                let name = name.as_str();
739                if name.starts_with("zd_def_") && !out.iter().any(|seen| seen == name) {
740                    out.push(name.to_string());
741                }
742            }
743            calls_in_expr(func, out);
744            for arg in args {
745                calls_in_expr(arg, out);
746            }
747        }
748        zig::Expr::Binary { lhs, rhs, .. } => {
749            calls_in_expr(lhs, out);
750            calls_in_expr(rhs, out);
751        }
752        zig::Expr::Paren(value)
753        | zig::Expr::Unary { value, .. }
754        | zig::Expr::Try(value)
755        | zig::Expr::Dot { value, .. } => calls_in_expr(value, out),
756        zig::Expr::Index { value, index } => {
757            calls_in_expr(value, out);
758            calls_in_expr(index, out);
759        }
760        zig::Expr::StructLit(fields) => {
761            for (_, value) in fields {
762                calls_in_expr(value, out);
763            }
764        }
765        zig::Expr::ListLit(elements) => {
766            for element in elements {
767                calls_in_expr(element, out);
768            }
769        }
770        zig::Expr::Literal(_) | zig::Expr::Name(_) | zig::Expr::Type(_) => {}
771    }
772}
773
774// -- the small helpers --------------------------------------------------
775
776fn text(value: &str) -> zig::Expr {
777    zig::Expr::Literal(Literal::Str(value.to_string()))
778}