Skip to main content

zerodds_lang_rules/
default_python.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The value a Python member takes when the stream does not carry it.
4//!
5//! Table 9 of DDS-XTypes 1.3, as Python. [`crate::default_c`] is the
6//! same table as C, [`crate::default_java`] as Java, [`crate::default_cpp`]
7//! as C++, [`crate::default_typescript`] as TypeScript, and
8//! `crates/wire-probe/src/default.rs` is the reflective column's; that
9//! module carries the clause quotations and the reasoning for the three
10//! kinds Table 9 does not list. Deliberately a sixth derivation and not
11//! a call into any of them: the columns are compared against each other,
12//! and a shared helper would make them agree about a mistake.
13//!
14//! ## Why it is a generated function and not a literal
15//!
16//! A literal cannot carry an aggregate. "A structure without any of the
17//! optional members and with other members set to their default values"
18//! is a constructor call with one argument per required member, and
19//! writing that at every position a member can go missing would put the
20//! same answer in the generated file as many times as the type is
21//! reached — where two copies can differ. One function per type is one
22//! answer per type, which is what the C and Java columns do and for the
23//! same reason.
24//!
25//! ## Why it answers with the value instead of filling one
26//!
27//! C's defaulter is `void zd_def_T(T *v)`: it writes through a pointer.
28//! Python has no pointer and no out-parameter — rebinding a parameter
29//! rebinds the local and nothing else — so the function **returns**
30//! Table 9's value and the caller assigns it, which is the same decision
31//! one step later. It is also what makes the reader's own shape work:
32//! [`crate::emit::python::Python`]'s decoder opens with
33//! `v = zd_def_T()`, so a position the stream does not carry is already
34//! what Table 9 says before a single octet is read.
35//!
36//! ## What Python forces that TypeScript does not
37//!
38//! **A body is one `return`.** Every other column fills a value member
39//! by member, because it has something to fill: C writes through a
40//! pointer, Java and TypeScript `new` an empty object first. A generated
41//! Python aggregate is a `@dataclasses.dataclass(kw_only=True)` whose
42//! required fields have **no default**, so `S()` is a `TypeError` and
43//! there is no empty object to fill — the whole value is one call. That
44//! is what `adapter/python.rs` meant by leaving the fields required, and
45//! `emit/python.rs`'s `reading_body` says the same thing from the other
46//! side.
47//!
48//! **Every argument is named.** Two independent reasons, either of which
49//! decides it. `kw_only=True` on the dataclass makes a positional
50//! argument a `TypeError` outright; and `adapter/python.rs` builds the
51//! field list from `all_members_of`, which flattens a base's members in
52//! front of the derived ones — so a positional list would silently
53//! transpose two same-typed members if that order ever changed. Named,
54//! a field that moves or is renamed is a `TypeError` at the call naming
55//! the parameter.
56//!
57//! **An `@optional` member is left out of the call.** §7.2.2.4.4.4.8's
58//! "Implementations shall not provide a default value" is the absence
59//! here, and `adapter/python.rs` already declares such a field
60//! `typing.Optional[T] = None`. Passing `field=None` would say the same
61//! thing in a way that stops saying it the day the declared default
62//! moves; leaving the argument out takes the declaration's answer.
63//!
64//! **A union is one dataclass and not one arm per case.**
65//! `adapter/python.rs` declares `_d` beside one `typing.Optional` field
66//! per case, so the value is `U(_d=…, one=…)` — or `U(_d=…)` alone where
67//! the discriminator's own default selects no branch, which Table 9
68//! allows and which a Python dataclass can hold as it stands.
69//!
70//! **An enumeration's default is the enumerator, written from the module
71//! root.** `a.b.Colour.RED` and not the number it happens to be: a
72//! generated codec sits at the module root and `adapter/python.rs` nests
73//! the `enum.IntEnum` in a class per IDL module, so `RED` there names
74//! nothing at all. Through [`crate::emit::CodecTarget::enumerator_constant`]
75//! rather than a second derivation, so the string is the one
76//! `emit/python.rs` writes at every other site.
77//!
78//! **A `long double` is a `Float128` and never a `float`.** A Python
79//! `float` is a binary64 with 53 significand bits and a `long double` is
80//! a binary128 with 113, so a zero written as `0.0` would be a member of
81//! the wrong type before any octet is read.
82//! `runtime/python/zerodds_wire/float128.py` carries the sixteen octets
83//! and `Float128.zero()` is its zero bit pattern.
84//!
85//! **A `fixed<P,S>` is a decimal string.** Table 9's zero for it is the
86//! one spelling `crates/wire-probe/src/canonical.rs` accepts for that
87//! scale — `"0.00"` for a `fixed<5,2>` — and not the empty string, which
88//! is not a decimal at all.
89//!
90//! **A `char` and a `wchar` are a `str` of one.** Table 9's zero for a
91//! character is the octet zero, which the C column writes as `0` and
92//! this one as `"\x00"`: Python has no character type, the member holds
93//! a one-character `str`, and an integer there would be a different type
94//! rather than a different spelling of one value.
95//!
96//! **A fixed array is that many elements, spelled out, as a tuple.**
97//! Python has no `Array.from(…, () => …)` and this tree has no
98//! comprehension, and `(zd_def_T(),) * n` is the one form that must not
99//! be used: it
100//! evaluates the element **once** and repeats the reference, so writing
101//! through element 0 of an array of aggregates would change all of them.
102//! One call per element evaluates once per element. The cost is the
103//! adapter's already — `adapter/python.rs` writes `tuple[T, …]` with one
104//! argument per element for the same array — and it is stated in both
105//! places rather than hidden by a threshold at which the spelling
106//! changes.
107//!
108//! **A `typedef` names the same defaulter as its target.**
109//! `adapter::python::type_tag` follows an alias through, so
110//! [`defaulter_name`] answers `zd_def_Point` for `Position` as well —
111//! and an alias arm that answered `return zd_def_Point()` would be a
112//! function whose body calls itself. So a typedef is resolved *before*
113//! the table is read, exactly as `emit/python.rs`'s `value_type_of`
114//! resolves it, and the one function that results is the target's.
115
116use crate::adapter::cst::MaterialiseError;
117use crate::adapter::python::declared_path;
118use crate::default_common::{discriminator_default_value, resolve_alias, selected_case};
119use crate::emit::CodecTarget;
120use crate::emit::python::Python;
121use crate::encode::EncodeError;
122use crate::fixed_literal::{fixed_text, zero_decimal};
123use zerodds_ast::{
124    Ast, ConstExpr, Literal as AstLiteral, MemberNode, PrimitiveKind, TypeKind, TypeNode,
125};
126use zerodds_ast_core::Id;
127use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
128use zerodds_lang_cst::python::{self as py, escape_name};
129
130/// How deep a chain of typedefs is followed before the model is taken to
131/// be malformed.
132const MAX_ALIAS_DEPTH: usize = 64;
133
134/// The defaulter function name for a type.
135///
136/// Keyed the same way [`crate::emit::decode::decoder_name`] is, so a
137/// primitive's defaulter is named by its IDL kind and not by its Python
138/// spelling. That distinction does more work in this column than in any
139/// other: `octet`, `unsigned short`, `long` and `unsigned long long` are
140/// one Python type and four wire widths, and a name built from the
141/// Python type would give all four one defaulter.
142#[must_use]
143pub fn defaulter_name(ast: &Ast, id: Id<TypeNode>) -> String {
144    match ast.type_(id).map(|node| node.kind.clone()) {
145        Ok(TypeKind::Primitive(kind)) => {
146            format!("zd_def_p_{}", kind.as_str().replace(' ', "_"))
147        }
148        _ => format!("zd_def_{}", Python::type_name(ast, id)),
149    }
150}
151
152/// The defaulter that goes with a decoder, by name.
153///
154/// The two schemes differ in one word — `zd_dec_p_long` against
155/// `zd_def_p_long` — because both are built from the same key. Doing it
156/// by text lets [`crate::emit::python::Python`] name the defaulter from
157/// a routine that has only a signature; a name that does not resolve is
158/// a `NameError` naming the function.
159#[must_use]
160pub fn defaulter_from_decoder(decoder: &str) -> String {
161    match decoder.strip_prefix("zd_dec_") {
162        Some(rest) => format!("zd_def_{rest}"),
163        None => format!("zd_def_{decoder}"),
164    }
165}
166
167/// `place = zd_def_<T>()`.
168#[must_use]
169pub fn default_into(ast: &Ast, id: Id<TypeNode>, place: py::Expr) -> py::Statement {
170    set(place, call(&defaulter_name(ast, id), Vec::new()))
171}
172
173/// One member's default: its `@default`, or its type's.
174///
175/// §7.2.2.4.4.4.9 puts the explicit one first — "An explicit definition
176/// associates a default value with the member at the time where the
177/// aggregated type is defined" — and falls back to "the default value of
178/// the member type. See Table 9."
179///
180/// # Errors
181/// [`EncodeError::NoWriter`] where a `@default` names a value this
182/// column cannot spell as a Python expression. Refused rather than
183/// silently replaced by Table 9's zero: a member the designer gave a
184/// default and a member that got a zero instead are different samples.
185pub fn member_default(
186    ast: &Ast,
187    node: &MemberNode,
188    type_: Id<TypeNode>,
189    place: py::Expr,
190) -> Result<Vec<py::Statement>, EncodeError> {
191    Ok(vec![set(place, member_value(ast, node, type_)?)])
192}
193
194/// The defaulters `decls` call, and exactly those.
195///
196/// The set is read **off the emitted calls** rather than derived a second
197/// time from the model, for the reason
198/// [`crate::default_c::c_defaulters_called_by`] gives at length: a
199/// derivation beside the decoder's is a second reading of §7.2.2.7, and
200/// the two readings drift. The closure is taken by emitting and
201/// re-reading until the batch calls nothing it does not also define.
202///
203/// # Errors
204/// [`EncodeError`] for a construct with no Python default.
205pub fn python_defaulters_called_by(
206    ast: &Ast,
207    decls: &[py::Statement],
208) -> Result<Vec<py::Statement>, EncodeError> {
209    let index: Vec<(String, Id<TypeNode>)> = ast
210        .types()
211        .map(|(id, _)| (defaulter_name(ast, id), id))
212        .collect();
213    let mut called: Vec<String> = Vec::new();
214    for decl in decls {
215        calls_in_statement(decl, &mut called);
216    }
217    let mut wanted: Vec<Id<TypeNode>> = Vec::new();
218    let mut have: Vec<String> = Vec::new();
219    // Bounded by the number of named defaulters, because every round but
220    // the last adds at least one. Written down rather than left to the
221    // loop, so a name that never resolves cannot spin.
222    for _ in 0..=index.len() {
223        let mut grew = false;
224        for name in &called {
225            if have.contains(name) {
226                continue;
227            }
228            have.push(name.clone());
229            // A name with no type behind it cannot happen for a call
230            // this generator emitted — every one came from
231            // `defaulter_name` over a type in this model — and if it
232            // ever does, the omission is a `NameError` naming the
233            // function at the first sample, which is louder than
234            // anything this could return.
235            if let Some(id) = index.iter().find(|(n, _)| n == name).map(|(_, id)| *id) {
236                wanted.push(id);
237                grew = true;
238            }
239        }
240        let produced = python_defaulters_over(ast, &wanted)?;
241        if !grew {
242            return Ok(produced);
243        }
244        for decl in &produced {
245            calls_in_statement(decl, &mut called);
246        }
247    }
248    python_defaulters_over(ast, &wanted)
249}
250
251/// Build the defaulter for each of `types`, once per name.
252///
253/// # Errors
254/// [`EncodeError`] for a construct with no Python default.
255pub fn python_defaulters_over(
256    ast: &Ast,
257    types: &[Id<TypeNode>],
258) -> Result<Vec<py::Statement>, EncodeError> {
259    let mut out = Vec::new();
260    let mut emitted: Vec<String> = Vec::new();
261    for id in types.iter().copied() {
262        let name = defaulter_name(ast, id);
263        if emitted.contains(&name) {
264            continue;
265        }
266        let Some(value) = value_of(ast, id, &name)? else {
267            continue;
268        };
269        emitted.push(name.clone());
270        out.push(py::Statement::Function(py::Function {
271            doc: Doc::line(
272                "Table 9 of DDS-XTypes 1.3: what this type is where the stream did not \
273                 carry it.",
274            ),
275            decorators: Vec::new(),
276            name: Ident::new(&name),
277            params: Vec::new(),
278            returns: Some(Python::value_type(ast, id)),
279            body: vec![py::Statement::Return(Some(value))],
280        }));
281    }
282    Ok(out)
283}
284
285/// The value one defaulter answers with, or `None` where the type has no
286/// Python data form at all.
287#[allow(clippy::too_many_lines)]
288fn value_of(ast: &Ast, id: Id<TypeNode>, own_name: &str) -> Result<Option<py::Expr>, EncodeError> {
289    // Before the table and not as one of its rows: a typedef and its
290    // target share a defaulter name, so an arm answering with a call
291    // would name this very function. See the module documentation.
292    let id = resolve_alias(ast, id);
293    let refuse = |kind: &'static str| EncodeError::NoWriter {
294        member: own_name.to_string(),
295        kind,
296    };
297    // A type the model does not hold. It used to answer `Ok(None)`,
298    // which the caller reads as "nothing to emit for this one" and
299    // skips — so a type whose handle no longer resolves quietly loses
300    // its defaulter, and the first sign of it is a member left at
301    // whatever the language zero-initialises to. Every other arm of
302    // this function either answers a value or refuses by name; this
303    // one now does too.
304    let Ok(node) = ast.type_(id) else {
305        return Err(refuse("a type the model does not hold"));
306    };
307    let value = match &node.kind {
308        TypeKind::Primitive(kind) => primitive_default(*kind),
309
310        // `""`, for both widths: a Python `str` is a sequence of code
311        // points already and the empty string is the same string in
312        // either.
313        TypeKind::String { .. } => text(""),
314
315        // A `fixed<P,S>` is the decimal it was written as, and
316        // `canonical::decimal` accepts exactly one spelling of zero for
317        // a given scale.
318        TypeKind::Fixed { scale, .. } => text(&zero_decimal(*scale)),
319
320        // Only an alias `resolve_alias` could not follow reaches here.
321        TypeKind::Alias { .. } => {
322            return Err(refuse("typedef with an unresolved target"));
323        }
324
325        // "The first literal in the enumeration", or the one
326        // `@default_literal` names — §7.3.1.2.1.10. By name and not as
327        // the number it happens to be: `adapter/python.rs` declared the
328        // member, and a generated file that spelled the value instead
329        // would keep working after `@value` moved it.
330        TypeKind::Enumeration { .. } => {
331            let Some(enumerator) = crate::emit::default_enumerator_name(ast, id) else {
332                return Err(refuse(
333                    "enumeration with no enumerator to take as its default",
334                ));
335            };
336            Python::enumerator_constant(ast, id, &enumerator)
337        }
338
339        // No bit set, which for an `enum.IntFlag` is the value zero.
340        // `a.b.Flags(0)` and not a bare `0`: `IntFlag(0)` is a legal
341        // member at every `@bit_bound` and is an `int`, so it satisfies
342        // both the declared field — `adapter/python.rs` annotates the
343        // member `int` — and this function's own return annotation,
344        // which `emit/python.rs`'s `value_type_of` gives as the
345        // declared class. A bare `0` satisfies only the first.
346        TypeKind::Bitmask { .. } => match declared_path(ast, id) {
347            Ok(path) => py::Expr::Call {
348                func: path,
349                args: vec![py::Arg::Positional(py::Expr::int(0))],
350            },
351            // The name is unreachable from the module root, and a
352            // bitmask no annotation can name is still zero.
353            Err(_) => py::Expr::int(0),
354        },
355
356        // A bitset's fields are separate integers in `adapter/python.rs`
357        // — a dataclass with one field per bitfield, each stating its
358        // own width in the metadata — so its default is that dataclass
359        // with every field at zero. The packing into the anchor is the
360        // codec's and does not appear here at all.
361        TypeKind::Bitset { .. } => {
362            let mut args = Vec::new();
363            for field in &ast.all_bitfields_of(id) {
364                let Ok(field_node) = ast.bitfield(*field) else {
365                    continue;
366                };
367                // An anonymous bitfield is padding: it moves the next
368                // field along and holds no value, so the declaration has
369                // no field for it either.
370                if let Some(field_name) = field_node.name.as_ref() {
371                    args.push((escape_name(field_name), py::Expr::int(0)));
372                }
373            }
374            construct(declared(ast, id, own_name)?, args)
375        }
376
377        // "A zero-length sequence of the same element type", and the
378        // same for a map's entries. An empty `list` and not `None`:
379        // `None` is what an `@optional` member's absence means here, and
380        // one value cannot mean both. §7.2.2.4.4.4.7 asks for a
381        // zero-length sequence, and §7.2.2.4.4.4.8's "Implementations
382        // shall not provide a default value" for an `@optional` member
383        // is the absence.
384        //
385        // A `list` and not a `tuple` for both: `emit/python.rs`'s
386        // `value_type_of` answers `list[…]`, because the reader writes
387        // elements into the slot it was handed.
388        TypeKind::Sequence { .. } | TypeKind::Map { .. } => py::Expr::List(Vec::new()),
389
390        // "An array of the same dimensions and same element type whose
391        // elements take the default value for their corresponding type."
392        //
393        // One call per element, one `tuple` per dimension — the
394        // `tuple[T, …]` `adapter/python.rs` declares the member as. See
395        // the module documentation for why `(zd_def_T(),) * n` is not
396        // this: it evaluates the element once and repeats the reference.
397        TypeKind::Array { element, dims } => {
398            let Some(element) = element.resolved() else {
399                return Err(refuse("array with an unresolved element"));
400            };
401            let mut lengths = Vec::new();
402            for dim in dims {
403                let length = ast
404                    .eval_integral(*dim)
405                    .map_err(|_| refuse("array whose length does not evaluate"))?;
406                lengths.push(
407                    usize::try_from(length)
408                        .map_err(|_| refuse("array of a length this column cannot spell"))?,
409                );
410            }
411            let mut out = call(&defaulter_name(ast, element), Vec::new());
412            // Innermost dimension first, so the outer one ends up
413            // outermost: `long a[2][3]` is two rows of three, which is
414            // what the walk indexes `[i0][i1]`.
415            for length in lengths.into_iter().rev() {
416                out = py::Expr::Tuple(vec![out; length]);
417            }
418            out
419        }
420
421        // "A structure without any of the optional members and with
422        // other members set to their default values."
423        TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
424            let mut args = Vec::new();
425            // The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
426            if let Some(base) = crate::adapter::c::struct_base(ast, id) {
427                args.push((
428                    escape_name("parent"),
429                    call(&defaulter_name(ast, base), Vec::new()),
430                ));
431            }
432            for member_id in ast.members_of(id) {
433                let Ok(member_node) = ast.member(*member_id) else {
434                    continue;
435                };
436                // Left out rather than passed as `None`: the field's
437                // declared default is already the absence, and one
438                // answer per question.
439                if member_node.facets.is_optional() {
440                    continue;
441                }
442                let Some(member_type) = member_node.type_.resolved() else {
443                    return Err(EncodeError::NoWriter {
444                        member: member_node.name.clone(),
445                        kind: "member with an unresolved type",
446                    });
447                };
448                args.push((
449                    escape_name(&member_node.name),
450                    member_value(ast, member_node, member_type)?,
451                ));
452            }
453            construct(declared(ast, id, own_name)?, args)
454        }
455
456        // "A union with the discriminator set to the default value for
457        // the discriminator type. If this selects a branch, then the
458        // selected member is also set to the default value for the
459        // member type."
460        //
461        // Where it selects none, `_d` alone is the whole value. That is
462        // Table 9 read literally, and it is available here where it is
463        // not in the TypeScript column: `adapter/typescript.rs`'s
464        // `union_default` falls back to the *first* case because a `_d`
465        // matching no arm does not inhabit a discriminated union type at
466        // all. A Python union is one dataclass whose case fields all
467        // default to `None`, so there is no such pressure and no case is
468        // invented.
469        TypeKind::Union { discriminator, .. } => {
470            let Some(discriminator) = discriminator.resolved() else {
471                return Err(refuse("union with an unresolved discriminator"));
472            };
473            let selector = discriminator_default_value(ast, discriminator);
474            let mut args = vec![(
475                "_d".to_string(),
476                discriminant_literal(ast, discriminator, selector.unwrap_or(0)),
477            )];
478            if let Some((member_type, field)) = selected_by_default(ast, id, selector)? {
479                args.push((field, call(&defaulter_name(ast, member_type), Vec::new())));
480            }
481            construct(declared(ast, id, own_name)?, args)
482        }
483
484        TypeKind::Native | TypeKind::ObjectRef { .. } | TypeKind::AnnotationType { .. } => {
485            return Ok(None);
486        }
487        TypeKind::ValueBox { .. } => {
488            return Err(refuse(
489                "boxed value, whose presence flag has no settled wire form",
490            ));
491        }
492        // Refused by name rather than passed over. `any` is in the model
493        // because IDL 4.2 has it; DDS-XTypes has no `any` and no
494        // encoding for one, and only CORBA CDR writes it — as a TypeCode
495        // ahead of the value. Returning `Ok(None)` here would generate a
496        // reader that silently omitted the member.
497        TypeKind::Any => {
498            return Err(refuse(
499                "`any`, which DDS-XTypes has no encoding for; only CORBA CDR writes one, \
500                 as a TypeCode ahead of the value",
501            ));
502        }
503        TypeKind::ValueBase => {
504            return Err(refuse(
505                "`ValueBase`, which DDS-XTypes has no encoding for; only CORBA CDR writes \
506                 one, as a chunked value with its repository id",
507            ));
508        }
509    };
510    Ok(Some(value))
511}
512
513/// One member's default as a value: its `@default`, or a call to its
514/// type's defaulter.
515///
516/// The value and not the assignment, because a Python aggregate is
517/// constructed in one call and there is nothing to assign into yet.
518/// [`member_default`] is the same answer one step later.
519fn member_value(
520    ast: &Ast,
521    node: &MemberNode,
522    type_: Id<TypeNode>,
523) -> Result<py::Expr, EncodeError> {
524    match node.facets.xtypes.as_ref().and_then(|x| x.default_value) {
525        Some(expr) => explicit(ast, expr, type_, &node.name),
526        None => Ok(call(&defaulter_name(ast, type_), Vec::new())),
527    }
528}
529
530/// Table 9's primitive rows, as the Python holder each takes.
531///
532/// Ten of the fifteen are the same `int` zero, because a Python `int`
533/// holds every IDL integer width exactly — see `emit/python.rs`'s
534/// [`CodecTarget::scalar`]. The five that differ are the five where
535/// Python has a type of its own.
536fn primitive_default(kind: PrimitiveKind) -> py::Expr {
537    match kind {
538        PrimitiveKind::Boolean => py::Expr::Literal(Literal::Bool(false)),
539        // `"\x00"`, a one-character `str`: Python has no character type,
540        // so an integer here would be a different type and not a
541        // different spelling of the same value. The octet is the C
542        // column's `0`.
543        PrimitiveKind::Char | PrimitiveKind::WChar => py::Expr::Literal(Literal::Char('\0')),
544        // `0.0` and not `0`: the member is annotated `float`, and the
545        // spelling is what says which of Python's two numeric types the
546        // value is.
547        PrimitiveKind::Float32 | PrimitiveKind::Float64 => {
548            py::Expr::Literal(Literal::Float("0.0".to_string()))
549        }
550        // A binary128 zero is its zero bit pattern.
551        // `runtime/python/zerodds_wire/float128.py` names it
552        // `Float128.zero()`, a classmethod whose one parameter has a
553        // default, so the spelling does not depend on the sign or on the
554        // class's internal layout.
555        PrimitiveKind::Float128 => py::Expr::Call {
556            func: Path::new(&["Float128", "zero"]),
557            args: Vec::new(),
558        },
559        PrimitiveKind::Octet
560        | PrimitiveKind::Int8
561        | PrimitiveKind::UInt8
562        | PrimitiveKind::Int16
563        | PrimitiveKind::UInt16
564        | PrimitiveKind::Int32
565        | PrimitiveKind::UInt32
566        | PrimitiveKind::Int64
567        | PrimitiveKind::UInt64 => py::Expr::int(0),
568    }
569}
570
571/// The path a generated declaration of `id` is reached by, from the
572/// module root.
573///
574/// `adapter/python.rs` nests an IDL module in a class and a defaulter
575/// sits at the module root, so `Point` there names nothing at all — or,
576/// worse, names whatever else happened to be bound.
577fn declared(ast: &Ast, id: Id<TypeNode>, own_name: &str) -> Result<Path, EncodeError> {
578    declared_path(ast, id).map_err(|_| EncodeError::NoWriter {
579        member: own_name.to_string(),
580        kind: "aggregate whose declared name the module chain will not answer for",
581    })
582}
583
584/// `a.b.S(x=…, y=…)` — every argument named, for the two reasons the
585/// module documentation gives.
586fn construct(path: Path, args: Vec<(String, py::Expr)>) -> py::Expr {
587    py::Expr::Call {
588        func: path,
589        args: args
590            .into_iter()
591            .map(|(name, value)| {
592                py::Arg::Keyword(py::Keyword {
593                    name: Ident::new(&name),
594                    value,
595                })
596            })
597            .collect(),
598    }
599}
600
601/// The case a defaulted discriminator selects, if any.
602fn selected_by_default(
603    ast: &Ast,
604    id: Id<TypeNode>,
605    selector: Option<i128>,
606) -> Result<Option<(Id<TypeNode>, String)>, EncodeError> {
607    let Some(selector) = selector else {
608        return Ok(None);
609    };
610    Ok(selected_case(ast, id, selector)?
611        .map(|(member, member_type)| (member_type, escape_name(&member.name))))
612}
613
614/// A discriminator value in the Python form its type gives it.
615///
616/// The four shapes are not interchangeable and the codec says so:
617/// `emit/python.rs`'s `cast` rewrites every case label into the
618/// discriminator's own form before comparing it, so a `_d` written as a
619/// number where the labels are characters would select no case at all —
620/// `"a" == 97` is `False` for every sample.
621fn discriminant_literal(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> py::Expr {
622    let resolved = resolve_alias(ast, discriminator);
623    if matches!(
624        ast.type_(resolved).map(|node| node.kind.clone()),
625        Ok(TypeKind::Enumeration { .. })
626    ) {
627        // The enumerator and not the number, for the reason the
628        // enumeration row gives. An `enum.IntEnum` member *is* an `int`,
629        // so it compares equal to the label the codec writes.
630        if let Some(enumerator) = crate::emit::default_enumerator_name(ast, resolved) {
631            return Python::enumerator_constant(ast, resolved, &enumerator);
632        }
633    }
634    match resolved_primitive(ast, resolved) {
635        Some(PrimitiveKind::Boolean) => py::Expr::Literal(Literal::Bool(value != 0)),
636        // `value` here is Table 9's and never a designer's: a `char` or
637        // `wchar` discriminator's default is zero — the arm above says
638        // so — and the only other source is an enumerator, which the
639        // enumeration branch has already answered. So the fallback is
640        // unreachable rather than a substitution, and `'\0'` is the
641        // value it would be answering with anyway.
642        Some(PrimitiveKind::Char | PrimitiveKind::WChar) => {
643            char_literal(u32::try_from(value).unwrap_or(0))
644        }
645        _ => py::Expr::int(value),
646    }
647}
648
649/// The primitive `id` is once typedefs are seen through, or `None` where
650/// it is not one.
651///
652/// zerodds-lint: recursion-depth 64 (alias chains; bounded by `MAX_ALIAS_DEPTH`)
653fn resolved_primitive(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
654    let mut current = id;
655    for _ in 0..MAX_ALIAS_DEPTH {
656        match ast.type_(current).map(|node| node.kind.clone()) {
657            Ok(TypeKind::Primitive(kind)) => return Some(kind),
658            Ok(TypeKind::Alias { aliased }) => current = aliased.resolved()?,
659            _ => return None,
660        }
661    }
662    None
663}
664
665/// An explicit `@default` as a Python expression for `type_`.
666#[allow(clippy::too_many_lines)]
667fn explicit(
668    ast: &Ast,
669    expr: Id<zerodds_ast::ConstExprNode>,
670    type_: Id<TypeNode>,
671    member_name: &str,
672) -> Result<py::Expr, EncodeError> {
673    let refuse = |kind: &'static str| EncodeError::NoWriter {
674        member: member_name.to_string(),
675        kind,
676    };
677    let node = ast
678        .expr(expr)
679        .map_err(|_| refuse("a `@default` this frontend cannot follow"))?;
680    let resolved = resolve_alias(ast, type_);
681    let kind = ast.type_(resolved).map(|node| node.kind.clone());
682    let is_float128 = matches!(kind, Ok(TypeKind::Primitive(PrimitiveKind::Float128)));
683    match &node.expr {
684        ConstExpr::Literal(AstLiteral::Boolean(flag)) => {
685            Ok(py::Expr::Literal(Literal::Bool(*flag)))
686        }
687        // A one-character string, which is what a `char` member is here.
688        ConstExpr::Literal(AstLiteral::Char(ch)) => Ok(char_literal(u32::from(*ch))),
689        ConstExpr::Literal(AstLiteral::WChar(code)) => Ok(char_literal(*code)),
690        ConstExpr::Literal(AstLiteral::String(value)) => Ok(text(value)),
691        ConstExpr::Literal(AstLiteral::WString(codes)) => {
692            let mut out = String::new();
693            for code in codes {
694                let Some(ch) = char::from_u32(*code) else {
695                    return Err(refuse("a `@default` wide string that is not code points"));
696                };
697                out.push(ch);
698            }
699            Ok(text(&out))
700        }
701        ConstExpr::Literal(AstLiteral::Float { mantissa, exponent }) => {
702            let exponent = exponent
703                .as_ref()
704                .map_or(String::new(), |text| format!("e{text}"));
705            let written = format!("{mantissa}{exponent}");
706            if is_float128 {
707                // Through the decimal and never through a Python
708                // `float`: `Float128.from_decimal` is exact, and a
709                // literal would round the source's digits to binary64
710                // sixty bits before the value reached the member.
711                return Ok(py::Expr::Call {
712                    func: Path::new(&["Float128", "from_decimal"]),
713                    args: vec![py::Arg::Positional(text(&written))],
714                });
715            }
716            // No suffix and no cast: a `float` member and a `double`
717            // member are the same Python `float`, and the rounding to
718            // binary32 is the writer's, at the octet.
719            Ok(py::Expr::Literal(Literal::Float(written)))
720        }
721        // A `fixed<P,S>` is the decimal text here, as the member's type
722        // is a `str`.
723        ConstExpr::Literal(AstLiteral::Fixed { digits, scale }) => {
724            Ok(text(&fixed_text(digits, *scale)))
725        }
726        ConstExpr::Literal(AstLiteral::Enumerator(reference)) => {
727            let target = reference
728                .resolved()
729                .ok_or_else(|| refuse("a `@default` enumerator that is unresolved"))?;
730            let enumerator = ast
731                .enumerator(target)
732                .map_err(|_| refuse("a `@default` enumerator this frontend cannot follow"))?;
733            Ok(Python::enumerator_constant(ast, resolved, &enumerator.name))
734        }
735        _ => {
736            // An integer literal, a named constant or arithmetic over
737            // them, folded the same way a bound or a case label is.
738            let value = ast
739                .eval_integral(expr)
740                .map_err(|_| refuse("a `@default` this frontend cannot evaluate"))?;
741            Ok(match kind {
742                Ok(TypeKind::Primitive(PrimitiveKind::Boolean)) => {
743                    py::Expr::Literal(Literal::Bool(value != 0))
744                }
745                // Refused rather than folded to NUL where the value has
746                // no code unit at all. A `@default` the designer wrote
747                // and a member that got `"\x00"` instead are different
748                // samples, and §7.2.2.4.4.4.9 associates a value with
749                // the member — a value this column cannot spell is a
750                // fact about the column and belongs in the exit code.
751                //
752                // The bound is `0xFFFF` and not "a Unicode scalar
753                // value": §7.4.1.1.1 writes a `wchar` as one UTF-16 code
754                // unit, and a lone surrogate is one. Refusing the
755                // surrogates would refuse what `chr` spells and the wire
756                // carries, which is the same substitution one door over.
757                Ok(TypeKind::Primitive(PrimitiveKind::Char | PrimitiveKind::WChar)) => {
758                    let point = u32::try_from(value)
759                        .ok()
760                        .filter(|code| *code <= u32::from(u16::MAX))
761                        .ok_or_else(|| {
762                            refuse("a `@default` character outside the range of a UTF-16 code unit")
763                        })?;
764                    char_literal(point)
765                }
766                // `Float128.from_int` and not a literal: the member is a
767                // binary128 and an `int` there is a different type.
768                Ok(TypeKind::Primitive(PrimitiveKind::Float128)) => py::Expr::Call {
769                    func: Path::new(&["Float128", "from_int"]),
770                    args: vec![py::Arg::Positional(py::Expr::int(value))],
771                },
772                // Every integer width, with no widening: a Python `int`
773                // is unbounded, so 18446744073709551615 is itself here
774                // where the Java column has to carry it as `-1L`.
775                _ => py::Expr::int(value),
776            })
777        }
778    }
779}
780
781/// A code point as the one-character `str` a `char` member holds.
782///
783/// `chr(<code>)` and not a character literal, because `Literal::Char`
784/// carries a Rust `char` and a Rust `char` is a Unicode scalar value. A
785/// Python `str` is a sequence of code points and `chr(0xD800)` is a
786/// one-character string; the two types disagree exactly on
787/// `0xD800..=0xDFFF`, and `char::from_u32(code).unwrap_or('\0')` answered
788/// that disagreement by substituting NUL with no diagnostic.
789///
790/// The same fold was measured in the C# and TypeScript columns on
791/// `@default(0xD800) wchar c`, where they wrote `'\0'` while the C column
792/// wrote `55296` and the Java column `(char) 55296`. The numeric path in
793/// [`explicit`] already refused rather than folded; this is the literal
794/// path, which nothing had reached.
795///
796/// Above `0xFFFF` there is no single UTF-16 code unit, and §7.4.1.1.1
797/// writes a `wchar` as one; the callers bound the value before it gets
798/// here.
799fn char_literal(code: u32) -> py::Expr {
800    match char::from_u32(code) {
801        // The character itself where Rust can spell it, so a readable
802        // `@default` stays readable in the generated file.
803        Some(point) => py::Expr::Literal(Literal::Char(point)),
804        None => py::Expr::Call {
805            func: Path::new(&["chr"]),
806            args: vec![py::Arg::Positional(py::Expr::int(i128::from(code)))],
807        },
808    }
809}
810
811// -- reading the calls back out ----------------------------------------
812
813/// The `zd_def_*` functions one statement calls, appended to `out`.
814///
815/// The counterpart of the TypeScript column's `calls_in_decl`. Python
816/// has no separate declaration node — a `def` and a `class` are
817/// statements — so the walk starts one level down and covers every
818/// compound form, because a defaulter call reaches this from inside a
819/// loop body as readily as from a `return`.
820///
821/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
822/// The tree walked here is the one this generator emitted and owns, so
823/// it holds no cycle; the depth is that body's height, and a defaulter
824/// body is a handful of statements deep.
825fn calls_in_statement(statement: &py::Statement, out: &mut Vec<String>) {
826    match statement {
827        py::Statement::Class(class) => calls_in_body(&class.body, out),
828        py::Statement::Function(function) => calls_in_body(&function.body, out),
829        py::Statement::Annotated(binding) => {
830            if let Some(value) = &binding.value {
831                calls_in_expr(value, out);
832            }
833        }
834        py::Statement::Assign(binding) => calls_in_expr(&binding.value, out),
835        py::Statement::Set { place, value } => {
836            calls_in_expr(place, out);
837            calls_in_expr(value, out);
838        }
839        py::Statement::Expr(value) => calls_in_expr(value, out),
840        py::Statement::If {
841            condition,
842            then,
843            otherwise,
844        } => {
845            calls_in_expr(condition, out);
846            calls_in_body(then, out);
847            if let Some(otherwise) = otherwise {
848                calls_in_body(otherwise, out);
849            }
850        }
851        py::Statement::For { bound, body, .. } => {
852            calls_in_expr(bound, out);
853            calls_in_body(body, out);
854        }
855        py::Statement::While { condition, body } => {
856            calls_in_expr(condition, out);
857            calls_in_body(body, out);
858        }
859        py::Statement::Return(value) => {
860            if let Some(value) = value {
861                calls_in_expr(value, out);
862            }
863        }
864        py::Statement::Break
865        | py::Statement::Comment(_)
866        | py::Statement::Pass
867        | py::Statement::Verbatim(_) => {}
868    }
869}
870
871/// zerodds-lint: recursion-depth 64 (statement nesting the walk builds).
872/// The tree walked here is the one this generator emitted and owns, so
873/// it holds no cycle; the depth is that body's height, and a defaulter
874/// body is a handful of statements deep.
875fn calls_in_body(body: &[py::Statement], out: &mut Vec<String>) {
876    for statement in body {
877        calls_in_statement(statement, out);
878    }
879}
880
881/// zerodds-lint: recursion-depth 64 (expression nesting the walk builds).
882/// The tree walked here is the one this generator emitted and owns, so
883/// it holds no cycle; the depth is that expression's height, which
884/// follows the member nesting the type walk already bounded.
885fn calls_in_expr(expr: &py::Expr, out: &mut Vec<String>) {
886    match expr {
887        py::Expr::Call { func, args } => {
888            // Unqualified only: a defaulter is a module-level function
889            // of the generated file, and this generator built every call
890            // to one with `defaulter_name`. `a.b.S(…)` is a constructor
891            // and never a defaulter.
892            if func.segments().len() == 1 {
893                let name = func
894                    .segments()
895                    .first()
896                    .map(|segment| segment.as_str().to_string())
897                    .unwrap_or_default();
898                if name.starts_with("zd_def_") && !out.iter().any(|seen| seen == &name) {
899                    out.push(name);
900                }
901            }
902            for arg in args {
903                match arg {
904                    py::Arg::Positional(value) => calls_in_expr(value, out),
905                    py::Arg::Keyword(keyword) => calls_in_expr(&keyword.value, out),
906                }
907            }
908        }
909        py::Expr::Tuple(values) | py::Expr::List(values) => {
910            for value in values {
911                calls_in_expr(value, out);
912            }
913        }
914        py::Expr::Dict(entries) => {
915            for (key, value) in entries {
916                calls_in_expr(key, out);
917                calls_in_expr(value, out);
918            }
919        }
920        py::Expr::Attribute { value, .. } => calls_in_expr(value, out),
921        py::Expr::Index { value, index } => {
922            calls_in_expr(value, out);
923            calls_in_expr(index, out);
924        }
925        py::Expr::Slice { start, end } => {
926            for side in [start, end].into_iter().flatten() {
927                calls_in_expr(side, out);
928            }
929        }
930        py::Expr::Unary { value, .. } | py::Expr::Paren(value) => calls_in_expr(value, out),
931        py::Expr::Binary { lhs, rhs, .. } => {
932            calls_in_expr(lhs, out);
933            calls_in_expr(rhs, out);
934        }
935        // An annotation holds no call: `Expr::Annotation` is a *type*
936        // written where a value belongs, and a type carries no defaulter.
937        py::Expr::Literal(_) | py::Expr::Path(_) | py::Expr::Annotation(_) => {}
938    }
939}
940
941// -- the small helpers --------------------------------------------------
942
943fn text(value: &str) -> py::Expr {
944    py::Expr::Literal(Literal::Str(value.to_string()))
945}
946
947fn call(callee: &str, args: Vec<py::Expr>) -> py::Expr {
948    py::Expr::Call {
949        func: Path::single(callee),
950        args: args.into_iter().map(py::Arg::Positional).collect(),
951    }
952}
953
954fn set(place: py::Expr, value: py::Expr) -> py::Statement {
955    py::Statement::Set { place, value }
956}
957
958/// Kept so the module's one dependency on the materialiser's error type
959/// is stated rather than inferred.
960const _: Option<MaterialiseError> = None;
961
962#[cfg(test)]
963mod tests {
964    // `print_stderr` for the one skip below: a test that silently
965    // checks nothing has to say so, and there is no other channel a
966    // `cargo test` run shows.
967    #![allow(
968        clippy::expect_used,
969        clippy::panic,
970        clippy::print_stderr,
971        clippy::unwrap_used
972    )]
973    use super::*;
974
975    /// IDL in, one model out.
976    fn model(idl: &str) -> Ast {
977        let parsed = zerodds_idl_parse::parse(idl);
978        assert!(
979            parsed.diagnostics().is_empty(),
980            "the source does not parse: {:?}",
981            parsed.diagnostics()
982        );
983        let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
984        assert!(
985            lowered.diagnostics.is_empty(),
986            "the source does not lower: {:?}",
987            lowered.diagnostics
988        );
989        let mut ast: Ast = lowered.ast;
990        ast.resolve_all_by_name().expect("the names resolve");
991        ast
992    }
993
994    fn root_of(ast: &Ast, type_name: &str) -> Id<TypeNode> {
995        ast.types()
996            .find(|(_, node)| node.name.as_str() == type_name)
997            .map(|(id, _)| id)
998            .expect("the named type")
999    }
1000
1001    fn rendered(decls: &[py::Statement]) -> String {
1002        let text = decls
1003            .iter()
1004            .map(zerodds_lang_render::python::statement)
1005            .collect::<Vec<_>>()
1006            .join("\n");
1007        parses(&text);
1008        text
1009    }
1010
1011    /// The defaulter for one named type.
1012    fn defaulter(idl: &str, type_name: &str) -> String {
1013        let ast = model(idl);
1014        let root = root_of(&ast, type_name);
1015        rendered(&python_defaulters_over(&ast, &[root]).expect("Table 9 answers for the type"))
1016    }
1017
1018    /// The defaulter for one named type and every defaulter it reaches.
1019    ///
1020    /// The only way to a collection's own row: a `sequence<long>` and a
1021    /// `long[3]` have no IDL name to look up, and the batch is where
1022    /// their functions are.
1023    fn closure(idl: &str, type_name: &str) -> String {
1024        let ast = model(idl);
1025        let root = root_of(&ast, type_name);
1026        let seed = python_defaulters_over(&ast, &[root]).expect("the root's own defaulter");
1027        rendered(&python_defaulters_called_by(&ast, &seed).expect("the closure"))
1028    }
1029
1030    /// Hand the text to CPython and ask whether it is a program.
1031    ///
1032    /// The same check `tests/python_codec.rs` makes, and for the same
1033    /// reason: in Python an indent that is off by four is a different
1034    /// program, and no reader in this crate would see it.
1035    ///
1036    /// Skipped, loudly, where no interpreter is on the path — a test
1037    /// that silently checks nothing is worse than one that says it did
1038    /// not run.
1039    fn parses(text: &str) {
1040        let Ok(output) = std::process::Command::new("python3")
1041            .arg("-c")
1042            .arg("import sys; compile(sys.stdin.read(), '<t>', 'exec')")
1043            .stdin(std::process::Stdio::piped())
1044            .stdout(std::process::Stdio::piped())
1045            .stderr(std::process::Stdio::piped())
1046            .spawn()
1047            .and_then(|mut child| {
1048                use std::io::Write as _;
1049                if let Some(stdin) = child.stdin.as_mut() {
1050                    stdin.write_all(text.as_bytes())?;
1051                }
1052                child.wait_with_output()
1053            })
1054        else {
1055            eprintln!("no python3 on the path: the defaulter's syntax was not checked");
1056            return;
1057        };
1058        assert!(
1059            output.status.success(),
1060            "the defaulter is not Python:\n{}\n--- source ---\n{text}",
1061            String::from_utf8_lossy(&output.stderr)
1062        );
1063    }
1064
1065    #[test]
1066    fn a_structure_of_primitives_names_every_required_field() {
1067        // `kw_only=True` makes a positional argument a TypeError, and a
1068        // required field left out is one too — which is what
1069        // `adapter/python.rs` meant by giving it no default.
1070        let text = defaulter("@final struct S { long x; double y; boolean z; };", "S");
1071        assert!(
1072            text.contains("def zd_def_S() -> S:"),
1073            "the signature is not the Python one:\n{text}"
1074        );
1075        assert!(
1076            text.contains("return S(x=zd_def_p_long(), y=zd_def_p_double(), z=zd_def_p_boolean())"),
1077            "the fields are not named:\n{text}"
1078        );
1079    }
1080
1081    #[test]
1082    fn a_nested_structure_reaches_its_members_defaulter_and_not_a_literal() {
1083        let text = defaulter(
1084            "@final struct Inner { long a; }; @final struct S { Inner i; long b; };",
1085            "S",
1086        );
1087        assert!(
1088            text.contains("return S(i=zd_def_Inner(), b=zd_def_p_long())"),
1089            "the nested default is not a call:\n{text}"
1090        );
1091    }
1092
1093    #[test]
1094    fn a_nested_module_is_written_from_the_module_root() {
1095        // A generated codec sits at the module root, where `S` names
1096        // nothing: `adapter/python.rs` nests each IDL module in a class.
1097        let text = defaulter(
1098            "module a { module b { @final struct S { long x; }; }; };",
1099            "S",
1100        );
1101        assert!(
1102            text.contains("return a.b.S(x=zd_def_p_long())"),
1103            "the constructor is not qualified:\n{text}"
1104        );
1105    }
1106
1107    #[test]
1108    fn an_optional_member_is_absent_from_the_constructor_call() {
1109        // 7.2.2.4.4.4.8: "Implementations shall not provide a default
1110        // value in such a case". The declared field is
1111        // `typing.Optional[T] = None` and the absence is the value.
1112        let text = defaulter("@final struct S { @optional long a; long b; };", "S");
1113        assert!(
1114            text.contains("return S(b=zd_def_p_long())"),
1115            "the optional member is not left out:\n{text}"
1116        );
1117        assert!(!text.contains("a="), "the optional member is set:\n{text}");
1118    }
1119
1120    #[test]
1121    fn a_union_whose_discriminators_default_selects_a_case_sets_both() {
1122        // Table 9: the discriminator's own default, and the member it
1123        // selects. A `long` discriminator defaults to zero.
1124        let text = defaulter(
1125            "@final union U switch (long) { case 0: long one; case 1: string other; };",
1126            "U",
1127        );
1128        assert!(
1129            text.contains("return U(_d=0, one=zd_def_p_long())"),
1130            "the selected case is not set:\n{text}"
1131        );
1132    }
1133
1134    #[test]
1135    fn a_union_whose_discriminators_default_selects_nothing_is_the_discriminator_alone() {
1136        // No label is zero and there is no `default:`, so Table 9's "if
1137        // this selects a branch" does not fire. A Python union is one
1138        // dataclass whose case fields default to None, so no case is
1139        // invented to fill the value out.
1140        let text = defaulter(
1141            "@final union U switch (long) { case 1: long one; case 2: long two; };",
1142            "U",
1143        );
1144        assert!(
1145            text.contains("return U(_d=0)"),
1146            "a case was invented:\n{text}"
1147        );
1148    }
1149
1150    #[test]
1151    fn a_unions_default_case_is_what_an_unlisted_discriminator_selects() {
1152        let text = defaulter(
1153            "@final union U switch (long) { case 1: long one; default: string other; };",
1154            "U",
1155        );
1156        assert!(
1157            text.contains("return U(_d=0, other=zd_def_string())"),
1158            "the default case is not selected:\n{text}"
1159        );
1160    }
1161
1162    #[test]
1163    fn an_enumeration_takes_its_first_literal_by_name() {
1164        // 7.3.1.2.1.10. By name and not as the number: a file that
1165        // spelled the value would keep working after `@value` moved it.
1166        let text = defaulter("enum Colour { RED, GREEN, BLUE };", "Colour");
1167        assert!(
1168            text.contains("return Colour.RED"),
1169            "the first literal is not the default:\n{text}"
1170        );
1171    }
1172
1173    #[test]
1174    fn an_enumeration_with_a_default_literal_takes_that_one() {
1175        let text = defaulter(
1176            "enum Colour { RED, @default_literal GREEN, BLUE };",
1177            "Colour",
1178        );
1179        assert!(
1180            text.contains("return Colour.GREEN"),
1181            "@default_literal is not honoured:\n{text}"
1182        );
1183    }
1184
1185    #[test]
1186    fn a_sequence_is_an_empty_list_at_every_bound() {
1187        // 7.2.2.4.4.4.7 asks for a zero-length sequence, and the bound
1188        // does not change what zero elements are.
1189        // The two names differ because the bound is part of the tag —
1190        // one hoisted routine per IDL type, not per element type. See
1191        // `crates/lang-rules/tests/a_bound_belongs_to_the_hoisted_routine.rs`.
1192        for (idl, name) in [
1193            (
1194                "@final struct S { sequence<long> many; };",
1195                "zd_def_seq_long",
1196            ),
1197            (
1198                "@final struct S { sequence<long, 4> many; };",
1199                "zd_def_seq_b4_long",
1200            ),
1201        ] {
1202            let text = closure(idl, "S");
1203            assert!(
1204                text.contains(&format!("def {name}() -> list[int]:")) && text.contains("return []"),
1205                "the sequence default is not an empty list:\n{text}"
1206            );
1207            // Not `None`: that is what an `@optional` member's absence
1208            // means here, and one value cannot mean both.
1209            assert!(!text.contains("None"), "the sequence is absent:\n{text}");
1210        }
1211    }
1212
1213    #[test]
1214    fn a_fixed_size_array_is_that_many_elements() {
1215        // One call per element and never `(zd_def_T(),) * 3`, which
1216        // evaluates the element once and repeats the reference; a tuple,
1217        // because `adapter/python.rs` declares the member `tuple[int, int,
1218        // int]` and a list in its place is not `==` to one.
1219        let text = closure("@final struct S { long a[3]; };", "S");
1220        assert!(
1221            text.contains("return (zd_def_p_long(), zd_def_p_long(), zd_def_p_long())"),
1222            "the elements are not spelled out as a tuple:\n{text}"
1223        );
1224        assert!(!text.contains(" * 3"), "the element is aliased:\n{text}");
1225    }
1226
1227    #[test]
1228    fn a_two_dimensional_array_is_one_tuple_per_dimension() {
1229        let text = closure("@final struct S { long g[2][3]; };", "S");
1230        // Two rows of three, which is what the walk indexes [i0][i1].
1231        assert!(
1232            text.contains(
1233                "((zd_def_p_long(), zd_def_p_long(), zd_def_p_long()), \
1234                 (zd_def_p_long(), zd_def_p_long(), zd_def_p_long()))"
1235            ),
1236            "the dimensions are not nested outermost first:\n{text}"
1237        );
1238    }
1239
1240    #[test]
1241    fn a_fixed_is_the_one_decimal_spelling_of_zero_for_its_scale() {
1242        // `crates/wire-probe/src/canonical.rs` accepts exactly one
1243        // spelling of zero at a given scale, and "" is not a decimal.
1244        let text = defaulter("@final struct S { fixed<5,2> f; };", "S");
1245        assert!(
1246            text.contains("f=zd_def_fixed_5_2()"),
1247            "the member does not reach the defaulter:\n{text}"
1248        );
1249        let text = defaulter(
1250            "typedef fixed<5,2> Money; @final struct S { Money f; };",
1251            "Money",
1252        );
1253        assert!(
1254            text.contains("return \"0.00\""),
1255            "the fixed default is not the decimal:\n{text}"
1256        );
1257    }
1258
1259    #[test]
1260    fn a_long_double_is_a_float128_and_never_a_float() {
1261        // A Python `float` is a binary64 with 53 significand bits and a
1262        // `long double` is a binary128 with 113.
1263        let text = defaulter("typedef long double LD; @final struct S { LD d; };", "LD");
1264        assert!(
1265            text.contains("return Float128.zero()"),
1266            "the long double default is not the runtime's zero:\n{text}"
1267        );
1268        assert!(
1269            !text.contains("0.0"),
1270            "a float reached a binary128:\n{text}"
1271        );
1272    }
1273
1274    #[test]
1275    fn a_char_is_a_one_character_string_and_not_an_integer() {
1276        // Python has no character type; the octet is the C column's 0.
1277        let text = closure("@final struct S { char c; };", "S");
1278        assert!(
1279            text.contains("return \"\\x00\""),
1280            "the char default is not a str:\n{text}"
1281        );
1282    }
1283
1284    #[test]
1285    fn a_bitset_is_the_dataclass_with_every_field_at_zero() {
1286        // `adapter/python.rs` declares a dataclass of one field per
1287        // bitfield; the packing into the anchor is the codec's.
1288        let text = defaulter(
1289            "bitset Packed { bitfield<3> low; bitfield<5> high; };",
1290            "Packed",
1291        );
1292        assert!(
1293            text.contains("return Packed(low=0, high=0)"),
1294            "the bitset default is not the dataclass:\n{text}"
1295        );
1296    }
1297
1298    #[test]
1299    fn a_typedef_and_its_target_are_one_function_and_it_does_not_call_itself() {
1300        // `type_tag` follows an alias through, so both name
1301        // `zd_def_Point`; an alias arm answering with a call would be a
1302        // function whose body calls itself.
1303        let text = defaulter(
1304            "@final struct Point { long x; }; typedef Point Position;",
1305            "Position",
1306        );
1307        assert!(
1308            text.contains("return Point(x=zd_def_p_long())"),
1309            "the typedef does not resolve to its target's table row:\n{text}"
1310        );
1311        assert!(
1312            !text.contains("return zd_def_Point()"),
1313            "the defaulter calls itself:\n{text}"
1314        );
1315    }
1316
1317    #[test]
1318    fn the_closure_reaches_every_defaulter_a_batch_calls() {
1319        // The set is read off the emitted calls, so a member's member's
1320        // defaulter arrives without a second walk over the model.
1321        let idl = "@final struct Leaf { long a; };
1322                   @final struct Middle { Leaf l; };
1323                   @final struct Top { Middle m; sequence<double> s; };";
1324        let parsed = zerodds_idl_parse::parse(idl);
1325        assert!(
1326            parsed.diagnostics().is_empty(),
1327            "{:?}",
1328            parsed.diagnostics()
1329        );
1330        let lowered = zerodds_idl_lower::lower(&parsed.syntax(), "test.idl");
1331        assert!(lowered.diagnostics.is_empty(), "{:?}", lowered.diagnostics);
1332        let mut ast: Ast = lowered.ast;
1333        ast.resolve_all_by_name().expect("the names resolve");
1334        let top = ast
1335            .types()
1336            .find(|(_, node)| node.name.as_str() == "Top")
1337            .map(|(id, _)| id)
1338            .expect("the root type");
1339        let seed = python_defaulters_over(&ast, &[top]).expect("the root's own defaulter");
1340        let all = python_defaulters_called_by(&ast, &seed).expect("the closure");
1341        let text = all
1342            .iter()
1343            .map(zerodds_lang_render::python::statement)
1344            .collect::<Vec<_>>()
1345            .join("\n");
1346        for expected in [
1347            "def zd_def_Middle()",
1348            "def zd_def_Leaf()",
1349            "def zd_def_p_long()",
1350            "def zd_def_seq_double()",
1351        ] {
1352            assert!(text.contains(expected), "{expected} is missing:\n{text}");
1353        }
1354        parses(&text);
1355    }
1356}