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}