Skip to main content

zerodds_lang_rules/
default_elixir.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value an Elixir member takes when the stream does not carry it.
4//!
5//! Table 9 of DDS-XTypes 1.3, as Elixir. [`crate::default_python`] is
6//! the same table as Python and carries the clause quotations;
7//! deliberately a further derivation and not a call into any sibling,
8//! because the columns are compared against each other and a shared
9//! helper would make them agree about a mistake.
10//!
11//! ## The shape
12//!
13//! One zero-arity function per type — `zd_def_T() -> value` — because a
14//! literal cannot be trusted to appear identically at every position a
15//! member can go missing, and because the decoder's own shape wants it:
16//! `emit/elixir.rs`'s reader opens with `v = zd_def_T()`, so every
17//! position the stream does not carry is already what Table 9 says
18//! before an octet is read.
19//!
20//! ## What the Elixir mapping decides
21//!
22//! - **An aggregate is a struct literal**: `%Common.Pose{x: 0, y: 0.0}`,
23//!   every non-optional field written. An `@optional` member is left
24//!   out — the `defstruct` default is already `nil`, and
25//!   §7.2.2.4.4.4.8's "Implementations shall not provide a default
26//!   value" is that absence.
27//! - **An enumeration's default is its constant function**:
28//!   `Common.Mode.idle()`, which answers the declared integer — the
29//!   binding holds enumerations as integers, and the function is the
30//!   materialiser's own spelling, so a moved `@value` moves both sides.
31//! - **A `char` is `"\0"`** — a one-character binary, because the
32//!   member holds one and an integer there would be a different type.
33//! - **A `long double` is `ZeroddsWire.Float128.zero()`** — sixteen
34//!   octets, never a `float()`, which holds 53 of its 113 significand
35//!   bits.
36//! - **A `fixed<P,S>` is the one decimal spelling of zero for its
37//!   scale** — `"0.00"` for `fixed<5,2>`; `""` is not a decimal.
38//! - **A collection is `[]`** and an array is its elements spelled out
39//!   as a tuple, one defaulter call per element — the `{T, …}` the
40//!   materialiser declares, which a list is not `==` to. `List.duplicate(zd_def_T(), n)`
41//!   would evaluate the element once, which for an immutable value is
42//!   harmless — and is still not written, because the sibling columns
43//!   spell the elements out and a reader comparing generated files
44//!   should not have to know which columns may share.
45
46use crate::adapter::elixir::type_module_path;
47use crate::default_common::{discriminator_default_value, resolve_alias, selected_case};
48use crate::emit::CodecTarget;
49use crate::emit::elixir::Elixir;
50use crate::encode::EncodeError;
51use crate::fixed_literal::{fixed_text, zero_decimal};
52use zerodds_ast::{
53    Ast, ConstExpr, Literal as AstLiteral, MemberNode, PrimitiveKind, TypeKind, TypeNode,
54};
55use zerodds_ast_core::Id;
56use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
57use zerodds_lang_cst::elixir::{self as ex, atom_name};
58
59/// How deep a chain of typedefs is followed before the model is taken
60/// to be malformed.
61const MAX_ALIAS_DEPTH: usize = 64;
62
63/// The defaulter function name for a type, keyed the same way
64/// [`crate::emit::decode::decoder_name`] is.
65#[must_use]
66pub fn defaulter_name(ast: &Ast, id: Id<TypeNode>) -> String {
67    match ast.type_(id).map(|node| node.kind.clone()) {
68        Ok(TypeKind::Primitive(kind)) => {
69            format!("zd_def_p_{}", kind.as_str().replace(' ', "_"))
70        }
71        _ => format!("zd_def_{}", Elixir::type_name(ast, id)),
72    }
73}
74
75/// The defaulter that goes with a decoder, by name — the two schemes
76/// differ in one word because both are built from the same key.
77#[must_use]
78pub fn defaulter_from_decoder(decoder: &str) -> String {
79    match decoder.strip_prefix("zd_dec_") {
80        Some(rest) => format!("zd_def_{rest}"),
81        None => format!("zd_def_{decoder}"),
82    }
83}
84
85/// `place = zd_def_T()`.
86#[must_use]
87pub fn default_into(ast: &Ast, id: Id<TypeNode>, place: ex::Expr) -> ex::Statement {
88    assign(place, call(&defaulter_name(ast, id), Vec::new()))
89}
90
91/// One member's default: its `@default`, or its type's. §7.2.2.4.4.4.9
92/// puts the explicit one first.
93///
94/// # Errors
95/// [`EncodeError::NoWriter`] where a `@default` names a value this
96/// column cannot spell. Refused rather than silently replaced by
97/// Table 9's zero: the two are different samples.
98pub fn member_default(
99    ast: &Ast,
100    node: &MemberNode,
101    type_: Id<TypeNode>,
102    place: ex::Expr,
103) -> Result<Vec<ex::Statement>, EncodeError> {
104    Ok(vec![assign(place, member_value(ast, node, type_)?)])
105}
106
107/// The defaulters `decls` call, and exactly those — read off the
108/// emitted calls rather than derived a second time from the model, for
109/// the reason [`crate::default_python::python_defaulters_called_by`]
110/// gives at length.
111///
112/// # Errors
113/// [`EncodeError`] for a construct with no Elixir default.
114pub fn elixir_defaulters_called_by(
115    ast: &Ast,
116    decls: &[ex::CodecFunction],
117) -> Result<Vec<ex::CodecFunction>, EncodeError> {
118    let index: Vec<(String, Id<TypeNode>)> = ast
119        .types()
120        .map(|(id, _)| (defaulter_name(ast, id), id))
121        .collect();
122    let mut called: Vec<String> = Vec::new();
123    for decl in decls {
124        calls_in_function(decl, &mut called);
125    }
126    let mut wanted: Vec<Id<TypeNode>> = Vec::new();
127    let mut have: Vec<String> = Vec::new();
128    // Bounded by the number of named defaulters: every round but the
129    // last adds at least one.
130    for _ in 0..=index.len() {
131        let mut grew = false;
132        for name in &called.clone() {
133            if have.contains(name) {
134                continue;
135            }
136            have.push(name.clone());
137            if let Some(id) = index.iter().find(|(n, _)| n == name).map(|(_, id)| *id) {
138                wanted.push(id);
139                grew = true;
140            }
141        }
142        let produced = elixir_defaulters_over(ast, &wanted)?;
143        if !grew {
144            return Ok(produced);
145        }
146        for decl in &produced {
147            calls_in_function(decl, &mut called);
148        }
149    }
150    elixir_defaulters_over(ast, &wanted)
151}
152
153/// Build the defaulter for each of `types`, once per name.
154///
155/// # Errors
156/// [`EncodeError`] for a construct with no Elixir default.
157pub fn elixir_defaulters_over(
158    ast: &Ast,
159    types: &[Id<TypeNode>],
160) -> Result<Vec<ex::CodecFunction>, EncodeError> {
161    let mut out = Vec::new();
162    let mut emitted: Vec<String> = Vec::new();
163    for id in types.iter().copied() {
164        let name = defaulter_name(ast, id);
165        if emitted.contains(&name) {
166            continue;
167        }
168        let Some(value) = value_of(ast, id, &name)? else {
169            continue;
170        };
171        emitted.push(name.clone());
172        out.push(ex::CodecFunction {
173            doc: Doc::line(
174                "Table 9 of DDS-XTypes 1.3: what this type is where the stream did not carry it.",
175            ),
176            name: Ident::new(&name),
177            params: Vec::new(),
178            body: Vec::new(),
179            result: value,
180        });
181    }
182    Ok(out)
183}
184
185/// The value one defaulter answers with, or `None` where the type has
186/// no Elixir data form at all.
187#[allow(clippy::too_many_lines)]
188fn value_of(ast: &Ast, id: Id<TypeNode>, own_name: &str) -> Result<Option<ex::Expr>, EncodeError> {
189    // Before the table and not as one of its rows: a typedef and its
190    // target share a defaulter name, so an arm answering with a call
191    // would name this very function.
192    let id = resolve_alias(ast, id);
193    let refuse = |kind: &'static str| EncodeError::NoWriter {
194        member: own_name.to_string(),
195        kind,
196    };
197    // A type the model does not hold. It used to answer `Ok(None)`,
198    // which the caller reads as "nothing to emit for this one" and
199    // skips — so a type whose handle no longer resolves quietly loses
200    // its defaulter, and the first sign of it is a member left at
201    // whatever the language zero-initialises to. Every other arm of
202    // this function either answers a value or refuses by name; this
203    // one now does too.
204    let Ok(node) = ast.type_(id) else {
205        return Err(refuse("a type the model does not hold"));
206    };
207    let value = match &node.kind {
208        TypeKind::Primitive(kind) => primitive_default(*kind),
209
210        // `""`, for both widths.
211        TypeKind::String { .. } => text(""),
212
213        // The one decimal spelling of zero for the scale.
214        TypeKind::Fixed { scale, .. } => text(&zero_decimal(*scale)),
215
216        TypeKind::Alias { .. } => {
217            return Err(refuse("typedef with an unresolved target"));
218        }
219
220        // §7.3.1.2.1.10: the `@default_literal`, or the first literal.
221        TypeKind::Enumeration { .. } => {
222            let Some(enumerator) = crate::emit::default_enumerator_name(ast, id) else {
223                return Err(refuse(
224                    "enumeration with no enumerator to take as its default",
225                ));
226            };
227            Elixir::enumerator_constant(ast, id, &enumerator)
228        }
229
230        // No bit set. The binding holds a bitmask as a plain integer.
231        TypeKind::Bitmask { .. } => ex::Expr::int(0),
232
233        // The struct with every named bitfield at zero.
234        TypeKind::Bitset { .. } => {
235            let mut fields = Vec::new();
236            for field in &ast.all_bitfields_of(id) {
237                let Ok(field_node) = ast.bitfield(*field) else {
238                    continue;
239                };
240                if let Some(field_name) = field_node.name.as_ref() {
241                    fields.push((Ident::new(&atom_name(field_name)), ex::Expr::int(0)));
242                }
243            }
244            ex::Expr::StructLiteral {
245                module: type_module_path(ast, id),
246                fields,
247            }
248        }
249
250        // "A zero-length sequence of the same element type", and the
251        // same for a map's entries. `[]` and not `nil`: `nil` is what
252        // an `@optional` member's absence means here.
253        TypeKind::Sequence { .. } | TypeKind::Map { .. } => ex::Expr::List(Vec::new()),
254
255        // One call per element, one tuple per dimension; see the module
256        // documentation for why the elements are spelled out.
257        TypeKind::Array { element, dims } => {
258            let Some(element) = element.resolved() else {
259                return Err(refuse("array with an unresolved element"));
260            };
261            let mut lengths = Vec::new();
262            for dim in dims {
263                let length = ast
264                    .eval_integral(*dim)
265                    .map_err(|_| refuse("array whose length does not evaluate"))?;
266                lengths.push(
267                    usize::try_from(length)
268                        .map_err(|_| refuse("array of a length this column cannot spell"))?,
269                );
270            }
271            let mut out = call(&defaulter_name(ast, element), Vec::new());
272            for length in lengths.into_iter().rev() {
273                out = ex::Expr::Tuple(vec![out; length]);
274            }
275            out
276        }
277
278        // "A structure without any of the optional members and with
279        // other members set to their default values."
280        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
281            let mut fields = Vec::new();
282            // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
283            if let Some(base) = crate::adapter::c::struct_base(ast, id) {
284                fields.push((
285                    Ident::new(&atom_name("parent")),
286                    call(&defaulter_name(ast, base), Vec::new()),
287                ));
288            }
289            for member_id in ast.members_of(id) {
290                let Ok(member_node) = ast.member(*member_id) else {
291                    continue;
292                };
293                if member_node.facets.is_optional() {
294                    continue;
295                }
296                let Some(member_type) = member_node.type_.resolved() else {
297                    return Err(EncodeError::NoWriter {
298                        member: member_node.name.clone(),
299                        kind: "member with an unresolved type",
300                    });
301                };
302                fields.push((
303                    Ident::new(&atom_name(&member_node.name)),
304                    member_value(ast, member_node, member_type)?,
305                ));
306            }
307            ex::Expr::StructLiteral {
308                module: type_module_path(ast, id),
309                fields,
310            }
311        }
312
313        // "A union with the discriminator set to the default value for
314        // the discriminator type", and the member that selects, where
315        // one does.
316        TypeKind::Union { discriminator, .. } => {
317            let Some(discriminator) = discriminator.resolved() else {
318                return Err(refuse("union with an unresolved discriminator"));
319            };
320            let selector = discriminator_default_value(ast, discriminator);
321            let mut fields = vec![(
322                Ident::new("_d"),
323                discriminant_literal(ast, discriminator, selector.unwrap_or(0)),
324            )];
325            if let Some((member_type, field)) = selected_by_default(ast, id, selector)? {
326                fields.push((
327                    Ident::new(&field),
328                    call(&defaulter_name(ast, member_type), Vec::new()),
329                ));
330            }
331            ex::Expr::StructLiteral {
332                module: type_module_path(ast, id),
333                fields,
334            }
335        }
336
337        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
338            return Ok(None);
339        }
340        TypeKind::ValueBox { .. } => {
341            return Err(refuse(
342                "boxed value, whose presence flag has no settled wire form",
343            ));
344        }
345        TypeKind::Any => {
346            return Err(refuse(
347                "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
348                 as a TypeCode ahead of the value",
349            ));
350        }
351        TypeKind::ValueBase => {
352            return Err(refuse(
353                "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
354                 one, as a chunked value with its repository id",
355            ));
356        }
357    };
358    Ok(Some(value))
359}
360
361/// One member's default as a value: its `@default`, or a call to its
362/// type's defaulter.
363fn member_value(
364    ast: &Ast,
365    node: &MemberNode,
366    type_: Id<TypeNode>,
367) -> Result<ex::Expr, EncodeError> {
368    match node.facets.xtypes.as_ref().and_then(|x| x.default_value) {
369        Some(expr) => explicit(ast, expr, type_, &node.name),
370        None => Ok(call(&defaulter_name(ast, type_), Vec::new())),
371    }
372}
373
374/// Table 9's primitive rows, as the Elixir holder each takes.
375fn primitive_default(kind: PrimitiveKind) -> ex::Expr {
376    match kind {
377        PrimitiveKind::Boolean => ex::Expr::Literal(Literal::Bool(false)),
378        // A one-character binary; the octet is the C column's `0`.
379        PrimitiveKind::Char | PrimitiveKind::WChar => text("\0"),
380        // `0.0` and not `0`: the member is a `float()` and the spelling
381        // is what says which of the two numeric types the value is.
382        PrimitiveKind::Float32 | PrimitiveKind::Float64 => {
383            ex::Expr::Literal(Literal::Float("0.0".to_string()))
384        }
385        // The binary128 zero bit pattern, from the runtime.
386        PrimitiveKind::Float128 => ex::Expr::Call {
387            func: Path::new(&["ZeroddsWire", "Float128", "zero"]),
388            args: Vec::new(),
389        },
390        PrimitiveKind::Octet
391        | PrimitiveKind::Int8
392        | PrimitiveKind::UInt8
393        | PrimitiveKind::Int16
394        | PrimitiveKind::UInt16
395        | PrimitiveKind::Int32
396        | PrimitiveKind::UInt32
397        | PrimitiveKind::Int64
398        | PrimitiveKind::UInt64 => ex::Expr::int(0),
399    }
400}
401
402/// The case a defaulted discriminator selects, if any.
403fn selected_by_default(
404    ast: &Ast,
405    id: Id<TypeNode>,
406    selector: Option<i128>,
407) -> Result<Option<(Id<TypeNode>, String)>, EncodeError> {
408    let Some(selector) = selector else {
409        return Ok(None);
410    };
411    Ok(selected_case(ast, id, selector)?
412        .map(|(member, member_type)| (member_type, atom_name(&member.name))))
413}
414
415/// A discriminator value in the Elixir form its type gives it — the
416/// same rewrites `emit/elixir.rs`'s `cast` makes at every label.
417fn discriminant_literal(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> ex::Expr {
418    let resolved = resolve_alias(ast, discriminator);
419    if matches!(
420        ast.type_(resolved).map(|node| node.kind.clone()),
421        Ok(TypeKind::Enumeration { .. })
422    ) {
423        if let Some(enumerator) = crate::emit::default_enumerator_name(ast, resolved) {
424            return Elixir::enumerator_constant(ast, resolved, &enumerator);
425        }
426    }
427    match resolved_primitive(ast, resolved) {
428        Some(PrimitiveKind::Boolean) => ex::Expr::Literal(Literal::Bool(value != 0)),
429        // Table 9's zero, which is the only value that reaches here —
430        // the enumeration branch has answered every designer-chosen one.
431        Some(PrimitiveKind::Char | PrimitiveKind::WChar) => {
432            char_literal(u32::try_from(value).unwrap_or(0))
433        }
434        _ => ex::Expr::int(value),
435    }
436}
437
438/// zerodds-lint: recursion-depth 64 (alias chains; bounded by
439/// `MAX_ALIAS_DEPTH`)
440fn resolved_primitive(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
441    let mut current = id;
442    for _ in 0..MAX_ALIAS_DEPTH {
443        match ast.type_(current).map(|node| node.kind.clone()) {
444            Ok(TypeKind::Primitive(kind)) => return Some(kind),
445            Ok(TypeKind::Alias { aliased }) => current = aliased.resolved()?,
446            _ => return None,
447        }
448    }
449    None
450}
451
452/// An explicit `@default` as an Elixir expression for `type_`.
453#[allow(clippy::too_many_lines)]
454fn explicit(
455    ast: &Ast,
456    expr: Id<zerodds_ast::ConstExprNode>,
457    type_: Id<TypeNode>,
458    member_name: &str,
459) -> Result<ex::Expr, EncodeError> {
460    let refuse = |kind: &'static str| EncodeError::NoWriter {
461        member: member_name.to_string(),
462        kind,
463    };
464    let node = ast
465        .expr(expr)
466        .map_err(|_| refuse("a `@default` this frontend cannot follow"))?;
467    let resolved = resolve_alias(ast, type_);
468    let kind = ast.type_(resolved).map(|node| node.kind.clone());
469    let is_float128 = matches!(kind, Ok(TypeKind::Primitive(PrimitiveKind::Float128)));
470    match &node.expr {
471        ConstExpr::Literal(AstLiteral::Boolean(flag)) => {
472            Ok(ex::Expr::Literal(Literal::Bool(*flag)))
473        }
474        ConstExpr::Literal(AstLiteral::Char(ch)) => Ok(char_literal(u32::from(*ch))),
475        ConstExpr::Literal(AstLiteral::WChar(code)) => Ok(char_literal(*code)),
476        ConstExpr::Literal(AstLiteral::String(value)) => Ok(text(value)),
477        ConstExpr::Literal(AstLiteral::WString(codes)) => {
478            let mut out = String::new();
479            for code in codes {
480                let Some(ch) = char::from_u32(*code) else {
481                    return Err(refuse("a `@default` wide string that is not code points"));
482                };
483                out.push(ch);
484            }
485            Ok(text(&out))
486        }
487        ConstExpr::Literal(AstLiteral::Float { mantissa, exponent }) => {
488            let exponent = exponent
489                .as_ref()
490                .map_or(String::new(), |text| format!("e{text}"));
491            let written = format!("{mantissa}{exponent}");
492            if is_float128 {
493                // Through the decimal and never through a `float()`:
494                // `Float128.from_decimal` is exact.
495                return Ok(ex::Expr::Call {
496                    func: Path::new(&["ZeroddsWire", "Float128", "from_decimal"]),
497                    args: vec![text(&written)],
498                });
499            }
500            // The renderer's `float_text` adds the point and zero
501            // Elixir's grammar wants; the value never changes.
502            Ok(ex::Expr::Literal(Literal::Float(written)))
503        }
504        ConstExpr::Literal(AstLiteral::Fixed { digits, scale }) => {
505            Ok(text(&fixed_text(digits, *scale)))
506        }
507        ConstExpr::Literal(AstLiteral::Enumerator(reference)) => {
508            let target = reference
509                .resolved()
510                .ok_or_else(|| refuse("a `@default` enumerator that is unresolved"))?;
511            let enumerator = ast
512                .enumerator(target)
513                .map_err(|_| refuse("a `@default` enumerator this frontend cannot follow"))?;
514            Ok(Elixir::enumerator_constant(ast, resolved, &enumerator.name))
515        }
516        _ => {
517            let value = ast
518                .eval_integral(expr)
519                .map_err(|_| refuse("a `@default` this frontend cannot evaluate"))?;
520            Ok(match kind {
521                Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
522                    ex::Expr::Literal(Literal::Bool(value != 0))
523                }
524                // Refused where the value is outside a UTF-16 code
525                // unit; and a surrogate has no Elixir binary at all, so
526                // it too is a refusal here — the codec's own label
527                // rewrite compares with an atom instead, but a member
528                // *holding* one is a value this binding cannot store.
529                Ok(TypeKind::Primitive(PrimitiveKind::Char | PrimitiveKind::WChar)) => {
530                    let point = u32::try_from(value)
531                        .ok()
532                        .filter(|code| *code <= u32::from(u16::MAX))
533                        .ok_or_else(|| {
534                            refuse("a `@default` character outside the range of a UTF-16 code unit")
535                        })?;
536                    if char::from_u32(point).is_none() {
537                        return Err(refuse(
538                            "a `@default` surrogate, which no Elixir binary can hold",
539                        ));
540                    }
541                    char_literal(point)
542                }
543                Ok(TypeKind::Primitive(PrimitiveKind::Float128)) => ex::Expr::Call {
544                    func: Path::new(&["ZeroddsWire", "Float128", "from_int"]),
545                    args: vec![ex::Expr::int(value)],
546                },
547                _ => ex::Expr::int(value),
548            })
549        }
550    }
551}
552
553/// A code point as the one-character binary a `char` member holds.
554///
555/// A surrogate has no UTF-8 spelling and therefore no Elixir binary;
556/// the callers bound the value before it gets here, and the one that
557/// cannot — an explicit `@default` — refuses by name instead.
558fn char_literal(code: u32) -> ex::Expr {
559    match char::from_u32(code) {
560        Some(point) => ex::Expr::Literal(Literal::Str(point.to_string())),
561        None => text("\0"),
562    }
563}
564
565// -- reading the calls back out ----------------------------------------
566
567/// The `zd_def_*` functions one routine calls, appended to `out`.
568fn calls_in_function(function: &ex::CodecFunction, out: &mut Vec<String>) {
569    calls_in_statements(&function.body, out);
570    calls_in_expr(&function.result, out);
571}
572
573/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
574/// The tree walked here is the one this generator emitted and owns, so
575/// it holds no cycle; the depth is that body's height, and a defaulter
576/// body is a handful of statements deep.
577fn calls_in_statements(statements: &[ex::Statement], out: &mut Vec<String>) {
578    for statement in statements {
579        match statement {
580            ex::Statement::Bind { value, .. } | ex::Statement::BindTuple { value, .. } => {
581                calls_in_expr(value, out);
582            }
583            ex::Statement::Update { path, value, .. } => {
584                for step in path {
585                    if let ex::Access::At(index) = step {
586                        calls_in_expr(index, out);
587                    }
588                }
589                calls_in_expr(value, out);
590            }
591            ex::Statement::If {
592                condition,
593                then,
594                otherwise,
595            } => {
596                calls_in_expr(condition, out);
597                calls_in_statements(then, out);
598                calls_in_statements(otherwise, out);
599            }
600            ex::Statement::For { bound, body, .. } => {
601                calls_in_expr(bound, out);
602                calls_in_statements(body, out);
603            }
604            ex::Statement::While { condition, body } => {
605                calls_in_expr(condition, out);
606                calls_in_statements(body, out);
607            }
608            ex::Statement::Return(value) => calls_in_expr(value, out),
609            ex::Statement::Seq(inner) => calls_in_statements(inner, out),
610            ex::Statement::Break | ex::Statement::Comment(_) => {}
611        }
612    }
613}
614
615/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
616/// The tree walked here is the one this generator emitted and owns, so
617/// it holds no cycle; the depth is that expression's height, which
618/// follows the member nesting the type walk already bounded.
619fn calls_in_expr(expr: &ex::Expr, out: &mut Vec<String>) {
620    match expr {
621        ex::Expr::Call { func, args } => {
622            if func.segments().len() == 1 {
623                let name = func
624                    .segments()
625                    .first()
626                    .map(|segment| segment.as_str().to_string())
627                    .unwrap_or_default();
628                if name.starts_with("zd_def_") && !out.iter().any(|seen| seen == &name) {
629                    out.push(name);
630                }
631            }
632            for argument in args {
633                calls_in_expr(argument, out);
634            }
635        }
636        ex::Expr::List(items) | ex::Expr::Tuple(items) => {
637            for item in items {
638                calls_in_expr(item, out);
639            }
640        }
641        ex::Expr::Keyword(pairs) => {
642            for (_, value) in pairs {
643                calls_in_expr(value, out);
644            }
645        }
646        ex::Expr::StructLiteral { fields, .. } => {
647            for (_, value) in fields {
648                calls_in_expr(value, out);
649            }
650        }
651        ex::Expr::Binary { lhs, rhs, .. } => {
652            calls_in_expr(lhs, out);
653            calls_in_expr(rhs, out);
654        }
655        ex::Expr::Unary { value, .. } | ex::Expr::Paren(value) => calls_in_expr(value, out),
656        ex::Expr::Field { value, .. } => calls_in_expr(value, out),
657        ex::Expr::Index { value, index } => {
658            calls_in_expr(value, out);
659            calls_in_expr(index, out);
660        }
661        ex::Expr::IfElse {
662            condition,
663            then,
664            otherwise,
665        } => {
666            calls_in_expr(condition, out);
667            calls_in_expr(then, out);
668            calls_in_expr(otherwise, out);
669        }
670        ex::Expr::Literal(_)
671        | ex::Expr::Atom(_)
672        | ex::Expr::Var(_)
673        | ex::Expr::Attr(_)
674        | ex::Expr::Module(_) => {}
675    }
676}
677
678// -- the small helpers --------------------------------------------------
679
680fn text(value: &str) -> ex::Expr {
681    ex::Expr::Literal(Literal::Str(value.to_string()))
682}
683
684fn call(callee: &str, args: Vec<ex::Expr>) -> ex::Expr {
685    ex::Expr::Call {
686        func: Path::single(callee),
687        args,
688    }
689}
690
691fn assign(place: ex::Expr, value: ex::Expr) -> ex::Statement {
692    match place_path(&place) {
693        Some((base, path)) if path.is_empty() => ex::Statement::Bind { name: base, value },
694        Some((base, path)) => ex::Statement::Update { base, path, value },
695        // A place this module cannot decompose becomes a call to a name
696        // no module defines: `elixirc` stops on it rather than the
697        // value being quietly dropped. The no-panic contract's spelling
698        // of "unreachable".
699        None => ex::Statement::Bind {
700            name: Ident::new("_"),
701            value: ex::Expr::Call {
702                func: Path::single("zd_refused_not_a_place"),
703                args: vec![place, value],
704            },
705        },
706    }
707}
708
709/// The base variable and access path of a place expression — the same
710/// decomposition `emit/elixir.rs` makes, repeated here because the two
711/// modules deliberately do not reach into each other's private helpers.
712///
713/// zerodds-lint: recursion-depth 64 (place-expression nesting the walk
714/// builds). The tree walked here is the one this generator emitted and
715/// owns, so it holds no cycle; the depth is that expression's height.
716fn place_path(place: &ex::Expr) -> Option<(Ident, Vec<ex::Access>)> {
717    match place {
718        ex::Expr::Var(name) => Some((name.clone(), Vec::new())),
719        ex::Expr::Field { value, name } => {
720            let (base, mut path) = place_path(value)?;
721            path.push(ex::Access::Key(name.clone()));
722            Some((base, path))
723        }
724        ex::Expr::Index { value, index } => {
725            let (base, mut path) = place_path(value)?;
726            path.push(ex::Access::At((**index).clone()));
727            Some((base, path))
728        }
729        _ => None,
730    }
731}