Skip to main content

zerodds_lang_rules/
default_node.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value a Node (plain JavaScript) member takes when the stream does
4//! not carry it.
5//!
6//! Table 9 of DDS-XTypes 1.3, as JavaScript. [`crate::default_lua`] is
7//! the nearest sibling — a dynamically-typed target needs no class and
8//! no constructor for an aggregate default, a plain literal *is* the
9//! value — and this is a further derivation and deliberately not a call
10//! into it or into [`crate::default_typescript`]: the columns are
11//! compared against each other, and a shared helper would make them
12//! agree about a mistake.
13//!
14//! ## The Node spellings, where they differ from Lua's
15//!
16//! **An `@optional` member is a key left out**, exactly as in Lua, and
17//! for the same clause — §7.2.2.4.4.4.8's "Implementations shall not
18//! provide a default value" is the absence, and in a JavaScript object
19//! literal the absence of a value is the absence of the key.
20//!
21//! **A `char` and a `wchar` are the code point, as a one-character
22//! `string`.** [`crate::emit::node`]'s module documentation carries why
23//! this column keeps `crate::typescript`'s value model rather than
24//! Lua's or Zig's integer one: Table 9's zero is therefore `"\0"`
25//! (`Literal::Char('\0')`), not `0`.
26//!
27//! **The three sixty-four-bit primitives are `bigint` literals.**
28//! `0n`, spelled through [`zerodds_lang_cst::node::Expr::Literal`] with
29//! the trailing `n` [`crate::emit::node::Node::wide_signed`] and
30//! [`crate::emit::typescript`]'s own overrides already carry.
31//!
32//! **A `long double` is the decimal text `"0"`.** A JavaScript `number`
33//! is a binary64; the member holds the decimal exactly as
34//! `runtime/node/zdwire.js`'s `float128FromDecimalString` takes it.
35//!
36//! **An enumeration's default is read out of the flattened declaration**
37//! through [`CodecTarget::enumerator_constant`], so the file that
38//! declared the value is the file this reads it from, exactly as every
39//! sibling column reads it.
40//!
41//! [`CodecTarget::enumerator_constant`]: crate::emit::CodecTarget::enumerator_constant
42
43use crate::default_common::{discriminator_default_value, resolve_alias, selected_case};
44use crate::emit::CodecTarget;
45use crate::emit::node::Node;
46use crate::encode::EncodeError;
47use crate::fixed_literal::zero_decimal;
48use zerodds_ast::{
49    Ast, ConstExpr, Literal as AstLiteral, MemberNode, PrimitiveKind, TypeKind, TypeNode,
50};
51use zerodds_ast_core::Id;
52use zerodds_lang_cst::common::{Doc, Literal};
53use zerodds_lang_cst::node::{Expr, Function, Stat};
54
55/// How deep a chain of typedefs is followed before the model is taken to
56/// be malformed.
57const MAX_ALIAS_DEPTH: usize = 64;
58
59/// The defaulter function name for a type — keyed like
60/// [`crate::emit::decode::decoder_name`], by the IDL kind for a
61/// primitive: the walk needs one function per wire shape and a name
62/// built from the JavaScript holder would give a `long` and an `octet`
63/// one defaulter, because both are `number`.
64#[must_use]
65pub fn defaulter_name(ast: &Ast, id: Id<TypeNode>) -> String {
66    match ast.type_(id).map(|node| node.kind.clone()) {
67        Ok(TypeKind::Primitive(kind)) => {
68            format!("zd_def_p_{}", kind.as_str().replace(' ', "_"))
69        }
70        _ => format!("zd_def_{}", Node::type_name(ast, id)),
71    }
72}
73
74/// The defaulter that goes with a decoder, by name: the two schemes
75/// differ in one word because both are built from the same key.
76#[must_use]
77pub fn defaulter_from_decoder(decoder: &str) -> String {
78    match decoder.strip_prefix("zd_dec_") {
79        Some(rest) => format!("zd_def_{rest}"),
80        None => format!("zd_def_{decoder}"),
81    }
82}
83
84/// `place = zd_def_<T>();`.
85#[must_use]
86pub fn default_into(ast: &Ast, id: Id<TypeNode>, place: Expr) -> Stat {
87    Stat::Assign {
88        doc: Doc::none(),
89        place,
90        value: Expr::call(&defaulter_name(ast, id), Vec::new()),
91    }
92}
93
94/// One member's default: its `@default`, or its type's. §7.2.2.4.4.4.9
95/// puts the explicit one first.
96///
97/// # Errors
98/// [`EncodeError::NoWriter`] where a `@default` names a value this
99/// column cannot spell as a JavaScript expression — refused rather than
100/// replaced by Table 9's zero, which is a different sample.
101pub fn member_default(
102    ast: &Ast,
103    node: &MemberNode,
104    type_: Id<TypeNode>,
105    place: Expr,
106) -> Result<Vec<Stat>, EncodeError> {
107    Ok(vec![Stat::Assign {
108        doc: Doc::none(),
109        place,
110        value: member_value(ast, node, type_)?,
111    }])
112}
113
114/// The defaulters `decls` call, and exactly those, read off the emitted
115/// calls rather than derived a second time from the model — the same
116/// closure discipline [`crate::default_python`] and [`crate::default_c`]
117/// state at length.
118///
119/// # Errors
120/// [`EncodeError`] for a construct with no Node default.
121pub fn node_defaulters_called_by(ast: &Ast, decls: &[Stat]) -> Result<Vec<Stat>, EncodeError> {
122    let index: Vec<(String, Id<TypeNode>)> = ast
123        .types()
124        .map(|(id, _)| (defaulter_name(ast, id), id))
125        .collect();
126    let mut called: Vec<String> = Vec::new();
127    for decl in decls {
128        calls_in_stat(decl, &mut called);
129    }
130    let mut wanted: Vec<Id<TypeNode>> = Vec::new();
131    let mut have: Vec<String> = Vec::new();
132    for _ in 0..=index.len() {
133        let mut grew = false;
134        for name in &called {
135            if have.contains(name) {
136                continue;
137            }
138            have.push(name.clone());
139            if let Some(id) = index.iter().find(|(n, _)| n == name).map(|(_, id)| *id) {
140                wanted.push(id);
141                grew = true;
142            }
143        }
144        let produced = node_defaulters_over(ast, &wanted)?;
145        if !grew {
146            return Ok(produced);
147        }
148        for decl in &produced {
149            calls_in_stat(decl, &mut called);
150        }
151    }
152    node_defaulters_over(ast, &wanted)
153}
154
155/// Build the defaulter for each of `types`, once per name.
156///
157/// # Errors
158/// [`EncodeError`] for a construct with no Node default.
159pub fn node_defaulters_over(ast: &Ast, types: &[Id<TypeNode>]) -> Result<Vec<Stat>, EncodeError> {
160    let mut out = Vec::new();
161    let mut emitted: Vec<String> = Vec::new();
162    for id in types.iter().copied() {
163        let name = defaulter_name(ast, id);
164        if emitted.contains(&name) {
165            continue;
166        }
167        let Some(value) = value_of(ast, id, &name)? else {
168            continue;
169        };
170        emitted.push(name.clone());
171        out.push(Stat::Function(Function {
172            doc: Doc::line(
173                "Table 9 of DDS-XTypes 1.3: what this type is where the stream did not carry it.",
174            ),
175            name: zerodds_lang_cst::common::Ident::new(&name),
176            params: Vec::new(),
177            body: vec![Stat::Return(Some(value))],
178        }));
179    }
180    Ok(out)
181}
182
183/// The value one defaulter answers with, or `None` where the type has no
184/// Node data form at all.
185#[allow(clippy::too_many_lines)]
186fn value_of(ast: &Ast, id: Id<TypeNode>, own_name: &str) -> Result<Option<Expr>, EncodeError> {
187    // Resolved before the match: a typedef and its target share a
188    // defaulter name, and an arm answering with a call would name this
189    // very function.
190    let id = resolve_alias(ast, id);
191    let refuse = |kind: &'static str| EncodeError::NoWriter {
192        member: own_name.to_string(),
193        kind,
194    };
195    // A type the model does not hold. `Ok(None)` here reads to the
196    // caller as "nothing to emit for this one" and skips the defaulter,
197    // so a stale handle quietly costs a type its Table 9 value and the
198    // first sign of it is a member the generated file leaves out. The
199    // Kotlin and Swift columns already refuse it by name; this one does
200    // too.
201    let Ok(type_node) = ast.type_(id) else {
202        return Err(refuse("a type the model does not hold"));
203    };
204    let value = match &type_node.kind {
205        TypeKind::Primitive(kind) => primitive_default(*kind),
206
207        // The empty string, for both widths.
208        TypeKind::String { .. } => Expr::string(""),
209
210        // The one spelling of zero the canonical form has for a scale.
211        TypeKind::Fixed { scale, .. } => Expr::string(&zero_decimal(*scale)),
212
213        TypeKind::Alias { .. } => {
214            return Err(refuse("typedef with an unresolved target"));
215        }
216
217        // "The first literal in the enumeration", or `@default_literal`'s
218        // (§7.3.1.2.1.10), read out of the flattened declaration by name
219        // so a moved `@value` moves this with it.
220        TypeKind::Enumeration { .. } => {
221            let Some(enumerator) = crate::emit::default_enumerator_name(ast, id) else {
222                return Err(refuse(
223                    "enumeration with no enumerator to take as its default",
224                ));
225            };
226            Node::enumerator_constant(ast, id, &enumerator)
227        }
228
229        // No bit set. `0n` where the holder is a `bigint`, otherwise a
230        // plain `0` — the same boundary `crate::emit::node::Node::scalar`
231        // draws for every other integer.
232        TypeKind::Bitmask { .. } => {
233            if ast.effective_bit_bound(id) > 32 {
234                Node::wide_unsigned(0)
235            } else {
236                Expr::int(0)
237            }
238        }
239
240        // One field per named bitfield, each at zero; the packing into
241        // the anchor is the codec's.
242        TypeKind::Bitset { .. } => {
243            let mut entries = Vec::new();
244            for field in &ast.all_bitfields_of(id) {
245                // Refused, not skipped: the key would simply be absent
246                // from the object literal, and `undefined` is what an
247                // `@optional` member's absence means here — a different
248                // value from the zero Table 9 gives a bitfield.
249                let Ok(field_node) = ast.bitfield(*field) else {
250                    return Err(refuse("a bitfield the model does not hold"));
251                };
252                // An anonymous bitfield is padding and holds no value.
253                let Some(field_name) = field_node.name.as_ref() else {
254                    continue;
255                };
256                entries.push((
257                    zerodds_lang_cst::common::Ident::new(field_name),
258                    Expr::int(0),
259                ));
260            }
261            Expr::Object(entries)
262        }
263
264        // "A zero-length sequence", and the same for a map's entries: an
265        // empty array. Never `undefined` — that is what an `@optional`
266        // member's absence means, and an array holding no elements is a
267        // real, present value.
268        TypeKind::Sequence { .. } | TypeKind::Map { .. } => Expr::Array(Vec::new()),
269
270        // "An array of the same dimensions … whose elements take the
271        // default value": one call per element, one array literal per
272        // dimension. Each element is its own call, so two elements of a
273        // structure type are two distinct objects and not one aliased
274        // twice.
275        TypeKind::Array { element, dims } => {
276            let Some(element) = element.resolved() else {
277                return Err(refuse("array with an unresolved element"));
278            };
279            let mut lengths = Vec::new();
280            for dim in dims {
281                let length = ast
282                    .eval_integral(*dim)
283                    .map_err(|_| refuse("array whose length does not evaluate"))?;
284                lengths.push(
285                    usize::try_from(length)
286                        .map_err(|_| refuse("array of a length this column cannot spell"))?,
287                );
288            }
289            let mut out = Expr::call(&defaulter_name(ast, element), Vec::new());
290            for length in lengths.into_iter().rev() {
291                out = Expr::Array(vec![out; length]);
292            }
293            out
294        }
295
296        // "A structure without any of the optional members and with
297        // other members set to their default values."
298        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
299            let mut entries = Vec::new();
300            // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
301            if let Some(base) = crate::adapter::c::struct_base(ast, id) {
302                entries.push((
303                    zerodds_lang_cst::common::Ident::new("parent"),
304                    Expr::call(&defaulter_name(ast, base), Vec::new()),
305                ));
306            }
307            for member_id in ast.members_of(id) {
308                // Refused, not skipped. A skipped member leaves its key
309                // out of the object literal, which is exactly the shape
310                // an `@optional` member's absence takes two lines down —
311                // the two states would be indistinguishable in the
312                // generated file.
313                let Ok(member_node) = ast.member(*member_id) else {
314                    return Err(refuse("a member the model does not hold"));
315                };
316                // Left out: the absence of the key is the absence.
317                if member_node.facets.is_optional() {
318                    continue;
319                }
320                let Some(member_type) = member_node.type_.resolved() else {
321                    return Err(EncodeError::NoWriter {
322                        member: member_node.name.clone(),
323                        kind: "member with an unresolved type",
324                    });
325                };
326                entries.push((
327                    zerodds_lang_cst::common::Ident::new(&member_node.name),
328                    member_value(ast, member_node, member_type)?,
329                ));
330            }
331            Expr::Object(entries)
332        }
333
334        // "A union with the discriminator set to the default value for
335        // the discriminator type. If this selects a branch, then the
336        // selected member is also set" — and where it selects none, `_d`
337        // alone is the whole value, which a plain object holds as it
338        // stands.
339        TypeKind::Union { discriminator, .. } => {
340            let Some(discriminator) = discriminator.resolved() else {
341                return Err(refuse("union with an unresolved discriminator"));
342            };
343            // Refused, not defaulted to zero. A discriminator whose type
344            // has no Table 9 value of its own is one IDL 4.2 7.4.1.4.4.2
345            // does not admit; writing `_d: 0` there would state a
346            // discriminant the source never named and select whichever
347            // branch happens to carry that label.
348            let selector = discriminator_default_value(ast, discriminator)
349                .ok_or_else(|| refuse("union whose discriminator type has no default value"))?;
350            let mut entries = vec![(
351                zerodds_lang_cst::common::Ident::new("_d"),
352                discriminant_literal(ast, discriminator, selector),
353            )];
354            if let Some((member_type, field)) = selected_by_default(ast, id, selector)? {
355                entries.push((
356                    zerodds_lang_cst::common::Ident::new(&field),
357                    Expr::call(&defaulter_name(ast, member_type), Vec::new()),
358                ));
359            }
360            Expr::Object(entries)
361        }
362
363        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
364            return Ok(None);
365        }
366        TypeKind::ValueBox { .. } => {
367            return Err(refuse(
368                "boxed value, whose presence flag has no settled wire form",
369            ));
370        }
371        TypeKind::Any => {
372            return Err(refuse(
373                "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
374                 as a TypeCode ahead of the value",
375            ));
376        }
377        TypeKind::ValueBase => {
378            return Err(refuse(
379                "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
380                 one, as a chunked value with its repository id",
381            ));
382        }
383    };
384    Ok(Some(value))
385}
386
387/// One member's default as a value: its `@default`, or a call to its
388/// type's defaulter.
389fn member_value(ast: &Ast, node: &MemberNode, type_: Id<TypeNode>) -> Result<Expr, EncodeError> {
390    match node.facets.xtypes.as_ref().and_then(|x| x.default_value) {
391        Some(expr) => explicit(ast, expr, type_, &node.name),
392        None => Ok(Expr::call(&defaulter_name(ast, type_), Vec::new())),
393    }
394}
395
396/// Table 9's primitive rows, as the JavaScript holder each takes.
397fn primitive_default(kind: PrimitiveKind) -> Expr {
398    match kind {
399        PrimitiveKind::Boolean => Expr::bool(false),
400        // The code point, as the one-character string the member holds
401        // — the value model `crate::emit::node`'s module documentation
402        // keeps from `crate::typescript`.
403        PrimitiveKind::Char | PrimitiveKind::WChar => Expr::Literal(Literal::Char('\0')),
404        PrimitiveKind::Float32 | PrimitiveKind::Float64 => {
405            Expr::Literal(Literal::Float("0".to_string()))
406        }
407        // The decimal text; `runtime/node/zdwire.js` parses it exactly.
408        PrimitiveKind::Float128 => Expr::string("0"),
409        PrimitiveKind::Int64 | PrimitiveKind::UInt64 => Node::wide_signed(0),
410        PrimitiveKind::Octet
411        | PrimitiveKind::Int8
412        | PrimitiveKind::UInt8
413        | PrimitiveKind::Int16
414        | PrimitiveKind::UInt16
415        | PrimitiveKind::Int32
416        | PrimitiveKind::UInt32 => Expr::int(0),
417    }
418}
419
420/// The case a defaulted discriminator selects, if any.
421fn selected_by_default(
422    ast: &Ast,
423    id: Id<TypeNode>,
424    selector: i128,
425) -> Result<Option<(Id<TypeNode>, String)>, EncodeError> {
426    Ok(selected_case(ast, id, selector)?
427        .map(|(member, member_type)| (member_type, member.name.clone())))
428}
429
430/// A discriminator value in the Node form its type gives it: an
431/// enumeration takes the constant read out of its flattened declaration,
432/// a boolean the literal, and everything else — a `char` included, whose
433/// holder is a one-character string — the number.
434fn discriminant_literal(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> Expr {
435    let resolved = resolve_alias(ast, discriminator);
436    if matches!(
437        ast.type_(resolved).map(|node| node.kind.clone()),
438        Ok(TypeKind::Enumeration { .. })
439    ) {
440        if let Some(enumerator) = crate::emit::default_enumerator_name(ast, resolved) {
441            return Node::enumerator_constant(ast, resolved, &enumerator);
442        }
443    }
444    if matches!(
445        resolved_primitive(ast, resolved),
446        Some(PrimitiveKind::Boolean)
447    ) {
448        return Expr::bool(value != 0);
449    }
450    if matches!(
451        resolved_primitive(ast, resolved),
452        Some(PrimitiveKind::Char | PrimitiveKind::WChar)
453    ) {
454        let point = u32::try_from(value).ok().and_then(char::from_u32);
455        if let Some(ch) = point {
456            return Expr::Literal(Literal::Char(ch));
457        }
458    }
459    if matches!(
460        resolved_primitive(ast, resolved),
461        Some(PrimitiveKind::Int64 | PrimitiveKind::UInt64)
462    ) {
463        return Node::wide_signed(value);
464    }
465    Expr::int(value)
466}
467
468/// The primitive `id` is once typedefs are seen through.
469///
470/// zerodds-lint: recursion-depth 64 (alias chains; bounded by `MAX_ALIAS_DEPTH`)
471fn resolved_primitive(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
472    let mut current = id;
473    for _ in 0..MAX_ALIAS_DEPTH {
474        match ast.type_(current).map(|node| node.kind.clone()) {
475            Ok(TypeKind::Primitive(kind)) => return Some(kind),
476            Ok(TypeKind::Alias { aliased }) => current = aliased.resolved()?,
477            _ => return None,
478        }
479    }
480    None
481}
482
483/// An explicit `@default` as a Node expression for `type_`.
484#[allow(clippy::too_many_lines)]
485fn explicit(
486    ast: &Ast,
487    expr: Id<zerodds_ast::ConstExprNode>,
488    type_: Id<TypeNode>,
489    member_name: &str,
490) -> Result<Expr, EncodeError> {
491    let refuse = |kind: &'static str| EncodeError::NoWriter {
492        member: member_name.to_string(),
493        kind,
494    };
495    let node = ast
496        .expr(expr)
497        .map_err(|_| refuse("a `@default` this frontend cannot follow"))?;
498    let resolved = resolve_alias(ast, type_);
499    let kind = ast.type_(resolved).map(|node| node.kind.clone());
500    let is_float128 = matches!(kind, Ok(TypeKind::Primitive(PrimitiveKind::Float128)));
501    let is_wide = matches!(
502        kind,
503        Ok(TypeKind::Primitive(
504            PrimitiveKind::Int64 | PrimitiveKind::UInt64
505        ))
506    );
507    match &node.expr {
508        ConstExpr::Literal(AstLiteral::Boolean(flag)) => Ok(Expr::bool(*flag)),
509        // The code point as the one-character string the member holds.
510        // §7.4.1.1.1 writes a `wchar` as one UTF-16 code unit and
511        // `String.fromCharCode` would hold a lone surrogate, but
512        // `Literal::Char` holds a Rust `char` and a surrogate is not
513        // one — so the point that this tree cannot carry is refused by
514        // name rather than written out as NUL, which is the same
515        // `@default` a source spelling `'\0'` would produce.
516        ConstExpr::Literal(AstLiteral::Char(ch)) => Ok(Expr::Literal(Literal::Char(*ch))),
517        ConstExpr::Literal(AstLiteral::WChar(code)) => {
518            let point = char::from_u32(*code)
519                .ok_or_else(|| refuse("a `@default` `wchar` that is not a Unicode scalar value"))?;
520            Ok(Expr::Literal(Literal::Char(point)))
521        }
522        ConstExpr::Literal(AstLiteral::String(value)) => Ok(Expr::string(value)),
523        ConstExpr::Literal(AstLiteral::WString(codes)) => {
524            let mut out = String::new();
525            for code in codes {
526                let Some(ch) = char::from_u32(*code) else {
527                    return Err(refuse("a `@default` wide string that is not code points"));
528                };
529                out.push(ch);
530            }
531            Ok(Expr::string(&out))
532        }
533        ConstExpr::Literal(AstLiteral::Float { mantissa, exponent }) => {
534            let exponent = exponent
535                .as_ref()
536                .map_or(String::new(), |text| format!("e{text}"));
537            let written = format!("{mantissa}{exponent}");
538            if is_float128 {
539                // The member holds the decimal text and
540                // `runtime/node/zdwire.js` parses it exactly; a
541                // JavaScript `number` literal would round to binary64
542                // before the value reached the member.
543                return Ok(Expr::string(&written));
544            }
545            Ok(Expr::Literal(Literal::Float(written)))
546        }
547        ConstExpr::Literal(AstLiteral::Fixed { digits, scale }) => Ok(Expr::string(
548            &crate::adapter::constant::fixed_decimal(digits, *scale),
549        )),
550        ConstExpr::Literal(AstLiteral::Enumerator(reference)) => {
551            let target = reference
552                .resolved()
553                .ok_or_else(|| refuse("a `@default` enumerator that is unresolved"))?;
554            let enumerator = ast
555                .enumerator(target)
556                .map_err(|_| refuse("a `@default` enumerator this frontend cannot follow"))?;
557            Ok(Node::enumerator_constant(ast, resolved, &enumerator.name))
558        }
559        _ => {
560            let value = ast
561                .eval_integral(expr)
562                .map_err(|_| refuse("a `@default` this frontend cannot evaluate"))?;
563            Ok(match kind {
564                Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => Expr::bool(value != 0),
565                // The code point as the one-character string the member
566                // holds; one outside a UTF-16 code unit is refused
567                // rather than folded, because the wire has no form for
568                // it.
569                Ok(TypeKind::Primitive(PrimitiveKind::Char | PrimitiveKind::WChar)) => {
570                    let point = u32::try_from(value)
571                        .ok()
572                        .filter(|code| *code <= u32::from(u16::MAX))
573                        .and_then(char::from_u32)
574                        .ok_or_else(|| {
575                            refuse("a `@default` character outside the range of a UTF-16 code unit")
576                        })?;
577                    Expr::Literal(Literal::Char(point))
578                }
579                // The decimal text of the integer, exactly.
580                Ok(TypeKind::Primitive(PrimitiveKind::Float128)) => {
581                    Expr::string(&format!("{value}"))
582                }
583                _ if is_wide => Node::wide_signed(value),
584                _ => Expr::int(value),
585            })
586        }
587    }
588}
589
590// -- reading the calls back out ------------------------------------------
591
592/// The `zd_def_*` functions one statement calls, appended to `out`.
593///
594/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
595/// The tree walked here is the one this generator emitted and owns, so
596/// it holds no cycle; the depth is that body's height, and a defaulter
597/// body is a handful of statements deep.
598fn calls_in_stat(statement: &Stat, out: &mut Vec<String>) {
599    match statement {
600        Stat::Declare { value, .. } => {
601            if let Some(value) = value {
602                calls_in_expr(value, out);
603            }
604        }
605        Stat::Assign { place, value, .. } => {
606            calls_in_expr(place, out);
607            calls_in_expr(value, out);
608        }
609        Stat::Function(function) => {
610            for inner in &function.body {
611                calls_in_stat(inner, out);
612            }
613        }
614        Stat::Return(value) => {
615            if let Some(value) = value {
616                calls_in_expr(value, out);
617            }
618        }
619        Stat::Expr(value) | Stat::Throw(value) => calls_in_expr(value, out),
620        Stat::If {
621            condition,
622            then,
623            otherwise,
624        } => {
625            calls_in_expr(condition, out);
626            for inner in then {
627                calls_in_stat(inner, out);
628            }
629            if let Some(rest) = otherwise {
630                for inner in rest {
631                    calls_in_stat(inner, out);
632                }
633            }
634        }
635        Stat::While { condition, body } => {
636            calls_in_expr(condition, out);
637            for inner in body {
638                calls_in_stat(inner, out);
639            }
640        }
641        Stat::For { bound, body, .. } => {
642            calls_in_expr(bound, out);
643            for inner in body {
644                calls_in_stat(inner, out);
645            }
646        }
647        Stat::Break | Stat::Comment(_) | Stat::Verbatim(_) => {}
648    }
649}
650
651/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
652/// The tree walked here is the one this generator emitted and owns, so
653/// it holds no cycle; the depth is that expression's height, which
654/// follows the member nesting the type walk already bounded.
655fn calls_in_expr(expr: &Expr, out: &mut Vec<String>) {
656    match expr {
657        Expr::Call { callee, args } => {
658            // A defaulter call is `zd_def_*(...)` and nothing else this
659            // generator emits looks like one.
660            if let Expr::Name(name) = callee.as_ref() {
661                if name.as_str().starts_with("zd_def_")
662                    && !out.iter().any(|seen| seen == name.as_str())
663                {
664                    out.push(name.as_str().to_string());
665                }
666            }
667            calls_in_expr(callee, out);
668            for arg in args {
669                calls_in_expr(arg, out);
670            }
671        }
672        Expr::Array(elements) => {
673            for element in elements {
674                calls_in_expr(element, out);
675            }
676        }
677        Expr::Object(fields) => {
678            for (_, value) in fields {
679                calls_in_expr(value, out);
680            }
681        }
682        Expr::New { args, .. } => {
683            for arg in args {
684                calls_in_expr(arg, out);
685            }
686        }
687        Expr::Member { object, .. } => calls_in_expr(object, out),
688        Expr::Index { object, index } => {
689            calls_in_expr(object, out);
690            calls_in_expr(index, out);
691        }
692        Expr::Paren(inner) | Expr::Unary { value: inner, .. } => calls_in_expr(inner, out),
693        Expr::Binary { lhs, rhs, .. } => {
694            calls_in_expr(lhs, out);
695            calls_in_expr(rhs, out);
696        }
697        Expr::Ternary {
698            condition,
699            then,
700            otherwise,
701        } => {
702            calls_in_expr(condition, out);
703            calls_in_expr(then, out);
704            calls_in_expr(otherwise, out);
705        }
706        Expr::Literal(_) | Expr::Undefined | Expr::Name(_) => {}
707    }
708}