Skip to main content

zerodds_lang_rules/emit/
python.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Python backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as `runtime/c`,
6//! `runtime/java`, `runtime/typescript` and the rest do, and none of
7//! them calls another. Where two columns disagree on an octet, one has
8//! misread the specification — which is the only reason to have a sixth.
9//!
10//! # The four decisions this column turns on
11//!
12//! **An `int` is unbounded, so a width is never a cast.** Every other
13//! backend spends part of itself on narrowing: `(int)` in Java,
14//! `static_cast` in C++, `zdNumber`/`zdBigInt` in TypeScript, because a
15//! value that arrived wider than its holder is a value already lost.
16//! Python has one integer type and it holds every IDL width exactly, so
17//! [`CodecTarget::cast`] is the identity on every integer and the width
18//! lives where it belongs — in the codec's *name*, which
19//! `adapter::python::type_tag` builds from the IDL kind and never from
20//! the Python spelling. A tag built from the Python type would give
21//! `octet`, `unsigned short` and `unsigned long long` one codec.
22//!
23//! **An assignment is a statement.** `a = b` is not an expression in
24//! Python: `(a = b)` is a `SyntaxError`. The shared walk builds one
25//! through `T::perform(T::binary("=", …))` at every assignment site but
26//! two, so [`Python::perform`] recognises that shape and answers with
27//! [`py::Statement::Set`]. Nothing else in the tree can hold an `=`.
28//!
29//! **A block is an indent.** There is no delimiter, so an empty suite is
30//! not untidy output — it is a `SyntaxError`, and there is no program the
31//! walk could have meant. The four constructors that take a body
32//! ([`Python::when`], [`Python::count_up`], [`Python::until`],
33//! [`Python::routine`]) pad an empty one with `pass` here, where they can
34//! say which body was empty; the renderer's own guard is the backstop.
35//!
36//! **A `long double` is a `Float128` and never a `float`.** A Python
37//! `float` is a binary64 with 53 significand bits and a `long double` is
38//! a binary128 with 113. `runtime/python`'s `Float128` carries the
39//! sixteen octets, and this column reaches for it exactly as the
40//! TypeScript one reaches for `ZdFloat128`.
41//!
42//! # The one place this file and the materialiser disagree
43//!
44//! `adapter/python.rs` declares a `long double` member as
45//! `_Float128 = typing.Annotated[str, _IdlFloat(bits=128)]` — the
46//! **decimal text** — and argues for it: a materialised module imports
47//! nothing but `dataclasses`, `enum` and `typing`, so a runtime class
48//! there would make every generated file depend on a layer the
49//! materialiser is not part of. [`Python::scalar`] answers `Float128`.
50//!
51//! The runtime settles which of the two a *codec* may use, and it is not
52//! this file's preference: `Writer.write_f128` takes `Float128 | float`
53//! and `Reader.read_f128` answers a `Float128`. Neither takes a `str`,
54//! so a codec built on the materialiser's spelling would hand a decimal
55//! string to a method that has no branch for one.
56//!
57//! Nothing enforces the annotation at run time, so the two disagree on
58//! paper and not on the wire — `primitive-longdouble` and
59//! `sequence-double-and-longdouble` are byte-identical in both
60//! directions under all ten encoding and byte-order pairs. What it does
61//! mean is that a generated module carrying both would not pass a strict
62//! type check, and the answer is one of two decisions neither of which
63//! is a codec's to take: the materialiser reaches for the runtime class
64//! and gives up its independence from this layer, or the codec converts
65//! at the member boundary — for which the shared walk offers no seam,
66//! because `ScalarWrite::F128` hands the member straight to
67//! `write_f128` with no cast in between.
68//!
69//! # What used to sit beside this file
70//!
71//! `crates/lang-python/src/emit.rs` was a second Python emitter, written
72//! against text rather than against a tree, driven by `zerodds-idl-python`
73//! and `tools/wireprobe/wireprobe-zerodds-python`. It carried its own
74//! copy of the walk — which members go out, in what order, under which
75//! header — and that copy was the thing this file exists to stop
76//! needing: `tools/wireprobe/wireprobe-zerodds-python-gen` and
77//! `tools/idlc/src/python_support.rs` are built on this file now, the
78//! same as the C#, Java, C, C++ and TypeScript columns are built on
79//! their own, and `crates/lang-python` is gone.
80
81use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
82use crate::adapter::python::{declared_path, type_tag};
83use crate::encode::EncodeError;
84use zerodds_ast::{Ast, MemberNode, TypeNode};
85use zerodds_ast_core::Id;
86use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
87use zerodds_lang_cst::python::{self as py, escape_name};
88
89/// The Python backend.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct Python;
92
93/// How deep a chain of typedefs and collections is followed.
94const MAX_TYPE_DEPTH: usize = 64;
95
96/// The type name [`CodecTarget::named`] is handed for C's signed `char`.
97///
98/// `emit/encode.rs`'s `map_key_less` writes `T::named("signed char")`
99/// where a `map`'s key is a `char` and the entry order is
100/// `MapKeyOrdering::SignedInteger`. That is a C spelling in the walk and
101/// not a type any other column has; this column recognises the string
102/// and answers with the conversion rather than with a name Python has
103/// never heard of. See [`Python::cast`].
104const SIGNED_CHAR: &str = "signed char";
105
106impl Python {
107    /// A dotted name as a path.
108    fn path(name: &str) -> Path {
109        Path::new(&name.split('.').collect::<Vec<_>>())
110    }
111
112    /// A name in `runtime/python`'s `zerodds_wire`, as the generated
113    /// module writes it.
114    ///
115    /// The simple name and not `zerodds_wire.X`: a generated module
116    /// imports what it uses by name, the way
117    /// `tools/wireprobe/wireprobe-zerodds-python-gen` writes the import
118    /// line, so a name the runtime does not export is an `ImportError`
119    /// naming it at the top of the file rather than a `NameError` at the
120    /// first sample.
121    fn runtime_name(name: &str) -> Path {
122        Path::single(name)
123    }
124
125    /// A body, with `pass` where the walk produced none.
126    ///
127    /// A Python suite cannot be empty. Every caller of this is a
128    /// constructor that is about to indent, so the padding is applied
129    /// where the body's owner is still in hand.
130    fn suite(mut body: Vec<py::Statement>) -> Vec<py::Statement> {
131        if body.is_empty() {
132            body.push(py::Statement::Pass);
133        }
134        body
135    }
136
137    /// A reader's body, with the sample it answers with around it.
138    ///
139    /// `v = zd_def_T()` … `return v`. Two things are settled here and
140    /// neither is a spelling:
141    ///
142    /// - **Where the sample comes from.** Table 9's value, through the
143    ///   generated defaulter, and not `T()` — a generated dataclass has
144    ///   required fields and constructing one without them is a
145    ///   `TypeError`, which is exactly what the materialiser meant by
146    ///   leaving them required. Every position the stream does not carry
147    ///   is then already what DDS-XTypes 1.3 Table 9 says it is, without
148    ///   the walk having to reach each one.
149    /// - **What the name is.** The defaulter and the decoder are keyed
150    ///   the same way — `emit::decode::decoder_name` and
151    ///   [`crate::default_python::defaulter_name`] differ in the three
152    ///   letters after `zd_` — so the one is the other with the verb
153    ///   changed. A name that does not resolve is a `NameError` naming
154    ///   the function.
155    fn reading_body(name: &str, body: Vec<py::Statement>) -> Vec<py::Statement> {
156        let defaulter = crate::default_python::defaulter_from_decoder(name);
157        let mut statements = vec![py::Statement::Set {
158            place: py::Expr::name("v"),
159            value: Self::call(&defaulter, Vec::new()),
160        }];
161        statements.extend(body);
162        statements.push(py::Statement::Return(Some(py::Expr::name("v"))));
163        statements
164    }
165
166    /// A runtime call whose Python parameters are not in the walk's
167    /// order, written with its arguments named.
168    ///
169    /// Three of them, and all three are the bit-bounded kinds:
170    /// `runtime/python`'s `write_enum(value, bit_bound)`,
171    /// `write_bitmask(value, bit_bound)` and `write_bitset(value,
172    /// anchor)` take the value first, and the walk hands the bound
173    /// first — `call::<T>("write_enum", vec![int(bits), value])`.
174    ///
175    /// Named rather than transposed silently, because both arguments are
176    /// integers: a positional swap is a `@bit_bound(8)` enumerator
177    /// written as the value 8 with the enumerator as the bound, which
178    /// raises nothing at the call and produces a plausible octet. With
179    /// the names written out, a signature that moves is a `TypeError` at
180    /// the call naming the parameter.
181    ///
182    /// The read side needs none of this: `read_enum(bit_bound)`,
183    /// `read_bitmask(bit_bound)` and `read_bitset(anchor)` each take one
184    /// argument, and every other operation on both sides already agrees
185    /// with the walk's order.
186    fn keywords_for(method: &str) -> Option<[&'static str; 2]> {
187        match method {
188            "write_enum" | "write_bitmask" => Some(["bit_bound", "value"]),
189            "write_bitset" => Some(["anchor", "value"]),
190            _ => None,
191        }
192    }
193}
194
195impl CodecTarget for Python {
196    type Expr = py::Expr;
197    type Stmt = py::Statement;
198    /// A suite, which in Python is a list of statements and nothing
199    /// else: there is no brace to hold and the indent belongs to the
200    /// renderer.
201    type Block = Vec<py::Statement>;
202    type Type = py::Type;
203    type Decl = py::Statement;
204
205    // -- types --------------------------------------------------------
206
207    /// One `int` for every integer width, because Python has one.
208    ///
209    /// Not a simplification. A Python `int` is unbounded, so `octet` and
210    /// `unsigned long long` are held exactly by the same type and
211    /// neither loses a bit at the assignment. What that costs is that
212    /// the type no longer says the width — which is why
213    /// `adapter::python::type_tag` names a codec after the IDL kind and
214    /// why `adapter/python.rs` writes every member's width into
215    /// `typing.Annotated` metadata rather than into the annotation.
216    ///
217    /// A `char` and a `wchar` are a `str` of one, which is the only
218    /// character type Python has, and what `runtime/python`'s
219    /// `write_char` and `write_wchar` take.
220    fn scalar(kind: Scalar) -> Self::Type {
221        match kind {
222            Scalar::Bool => py::Type::name("bool"),
223            Scalar::Char => py::Type::name("str"),
224            Scalar::Int8
225            | Scalar::UInt8
226            | Scalar::Int16
227            | Scalar::UInt16
228            | Scalar::Int32
229            | Scalar::UInt32
230            | Scalar::Int64
231            | Scalar::UInt64
232            | Scalar::Index
233            | Scalar::Size => py::Type::name("int"),
234            Scalar::Float | Scalar::Double => py::Type::name("float"),
235            Scalar::LongDouble => py::Type::name("Float128"),
236        }
237    }
238
239    fn named(name: &str) -> Self::Type {
240        py::Type::Name(Self::path(name))
241    }
242
243    /// `None`, which is what a Python function that answers with nothing
244    /// is annotated with.
245    fn nothing() -> Self::Type {
246        py::Type::name("None")
247    }
248
249    /// The runtime's handles, by their exported names.
250    ///
251    /// Each is a `dataclass` or an `Enum` in `runtime/python`, and a
252    /// generated module imports it by name.
253    fn handle(kind: Handle) -> Self::Type {
254        py::Type::name(match kind {
255            Handle::DHeader => "DHeader",
256            Handle::EmHeader => "EmHeader",
257            Handle::Pid => "Pid",
258            Handle::TryConstruct => "TryConstruct",
259            Handle::ReaderMark => "ReaderMark",
260            Handle::PidRead => "PidRead",
261            Handle::EmHeaderRead => "EmHeaderRead",
262        })
263    }
264
265    /// A `str` of one character is what a `char` is here.
266    ///
267    /// `runtime/python`'s `write_char` takes the character and refuses a
268    /// code point above U+00FF rather than truncating it — which is a
269    /// decision about the wire and is the runtime's, not this file's.
270    fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
271        value
272    }
273
274    /// The runtime's policy values, in the spelling `runtime/python`
275    /// accepts.
276    ///
277    /// **Strings and integers, not an enumeration per axis.** That is
278    /// the runtime's shape and not this file's preference:
279    /// `Writer.write_wstring` matches `policy` against
280    /// `_WSTRING_POLICIES` and `Writer.write_wchar` takes `width` as the
281    /// octet count. The spellings are `WstringLengthPolicy::as_str` and
282    /// its siblings in `crates/ast-layout/src/key.rs`, which is where
283    /// the measurement lives, and they are **not** the TypeScript
284    /// column's: `runtime/typescript` writes `octets-terminated-2` where
285    /// this one writes `octets/terminated/2`, and a value copied across
286    /// would be refused by name at the first sample.
287    ///
288    /// `crates/lang-rules/tests/python_names_its_runtime.rs` reads both
289    /// files and compares them, because nothing between this match and
290    /// the interpreter looks at the string.
291    ///
292    /// One arm per constant and no catch-all: a policy this column has
293    /// not implemented has to be a compile error here rather than a
294    /// value that looks plausible.
295    fn constant(which: Constant) -> Self::Expr {
296        let text = |value: &str| py::Expr::Literal(Literal::Str(value.to_string()));
297        match which {
298            // The octet count itself. `write_wchar(width=…)` takes the
299            // number, so there is no name to write.
300            Constant::WcharWidth2 => py::Expr::int(2),
301            Constant::WcharWidth4 => py::Expr::int(4),
302            Constant::WcharFixed => text("fixed"),
303            Constant::WcharGiopLengthPrefixed => text("giop-length-prefixed"),
304            Constant::WstringUnitsStream => text("stream"),
305            Constant::WstringUnitsBigEndianAlways => text("big-endian-always"),
306            Constant::WstringUnitsByteOrderMark => text("byte-order-mark"),
307            Constant::WstringUnitsHostUnswapped => text("host-unswapped"),
308            Constant::WstringSpecification => text("specification"),
309            Constant::WstringCharacterCount => text("character-count"),
310            Constant::WstringCharacterCountTerminated => text("character-count-terminated"),
311            Constant::WstringSpecificationWideUnits => text("specification-wide-units"),
312            Constant::WstringRosTypeSupport => text("ros-type-support"),
313            // The three cells nobody has named, spelled by their
314            // coordinates. `runtime/python` reaches every cell of
315            // C1 x C2 x C3 by `counts/terminator/width` as well as by
316            // name, and these are the coordinate forms.
317            Constant::WstringOctetsTerminated2 => text("octets/terminated/2"),
318            Constant::WstringOctetsTerminated4 => text("octets/terminated/4"),
319            Constant::WstringCharactersTerminated2 => text("characters/terminated/2"),
320            Constant::FixedDeclared => text("declared"),
321            Constant::FixedValuePrecision => text("value-precision"),
322            Constant::FixedValueStringDigits => text("value-string-digits"),
323            Constant::ParameterMember => py::Expr::Path(Self::path("ParameterRole.MEMBER")),
324            Constant::ParameterOptional => py::Expr::Path(Self::path("ParameterRole.OPTIONAL")),
325            Constant::TryConstructDiscard => py::Expr::Path(Self::path("TryConstruct.DISCARD")),
326            Constant::TryConstructTrim => py::Expr::Path(Self::path("TryConstruct.TRIM")),
327            Constant::TryConstructUseDefault => {
328                py::Expr::Path(Self::path("TryConstruct.USE_DEFAULT"))
329            }
330            Constant::StatusOk => py::Expr::Path(Self::path("Status.OK")),
331            Constant::StatusDiscarded => py::Expr::Path(Self::path("Status.DISCARDED")),
332            Constant::StatusImpossibleLength => {
333                py::Expr::Path(Self::path("Status.IMPOSSIBLE_LENGTH"))
334            }
335            Constant::StatusOutOfRange => py::Expr::Path(Self::path("Status.OUT_OF_RANGE")),
336            Constant::StatusTruncated => py::Expr::Path(Self::path("Status.TRUNCATED")),
337        }
338    }
339
340    /// `int`, at every bit bound.
341    ///
342    /// The one place this column's unbounded integer is simply the right
343    /// answer: a `@bit_bound(64)` set and a `@bit_bound(3)` set are
344    /// assembled in the same type and neither can overflow it. The other
345    /// columns pick the smallest holder that fits and one of them —
346    /// Java — has to go up a signedness to do it.
347    fn unsigned_holder(_bits: u16) -> Self::Type {
348        py::Type::name("int")
349    }
350
351    /// The Python type of a value of `id`.
352    ///
353    /// Not [`Self::type_name`], which is the codec's tag — `p_long`,
354    /// `seq_long` — and names a function, not a type.
355    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
356        value_type_of(ast, id, 0)
357    }
358
359    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
360        type_tag(ast, id, 0).unwrap_or_else(|_| format!("unresolved_type_{}", id.slot()))
361    }
362
363    /// A `map`'s entry type, which in this column is not a type.
364    ///
365    /// §7.4.1.1.4 defines a `map` as a sequence of entries, and
366    /// `adapter/python.rs` writes that as `list[tuple[K, V]]` with no
367    /// entry class at all — "the one place this column declares less
368    /// than the Java one". So there is no name to answer with, and the
369    /// walk needs one for exactly two things: the `element` argument of
370    /// [`Self::make_room`] and the type of [`Self::storage`]'s
371    /// `zd_scratch`. Both are answered here from the shape rather than
372    /// from a declaration, and this name is what says which shape it is.
373    ///
374    /// A `list` and not a `tuple`, because the reader writes each half
375    /// into the slot separately — `v[i][0] = …` — and a tuple cannot be
376    /// written into at all. [`Self::make_room`] turns the run back into
377    /// tuples once both halves are there, so what a decoder answers with
378    /// is what `from_json` built and what the encoder reads.
379    fn map_entry_name(_ast: &Ast, _id: Id<TypeNode>) -> String {
380        MAP_ENTRY.to_string()
381    }
382
383    /// Nothing. `adapter/python.rs` nests an IDL module in a class and
384    /// every reference is written from the module root, which
385    /// [`Self::enumerator_constant`] and `value_type_of` do through
386    /// `declared_path`. A prefix would be a second, shorter answer to
387    /// the same question.
388    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
389        String::new()
390    }
391
392    fn escape(name: &str) -> String {
393        escape_name(name)
394    }
395
396    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
397    fn inherited_path(depth: usize) -> Option<String> {
398        Some(format!("{}.", escape_name("parent")).repeat(depth))
399    }
400
401    // -- expressions ---------------------------------------------------
402
403    fn name(text: &str) -> Self::Expr {
404        py::Expr::Path(Self::path(text))
405    }
406
407    fn int(value: i128) -> Self::Expr {
408        py::Expr::int(value)
409    }
410
411    /// The number.
412    ///
413    /// The trait's default is C's `UINT64_C(..)`, which is a macro no
414    /// Python file declares, and the whole reason for a widened literal
415    /// is a language whose integer constants have a type. Python's do
416    /// not: `18446744073709551615` is an `int` and so is
417    /// `-9223372036854775808`.
418    fn wide_int(value: i128) -> Self::Expr {
419        py::Expr::int(value)
420    }
421
422    /// As [`Self::wide_int`], and the value rather than the
423    /// two's-complement pattern: a Python `int` is unbounded, so a
424    /// negative enumerator is written as the negative number it is.
425    fn wide_unsigned(value: u64) -> Self::Expr {
426        py::Expr::int(i128::from(value))
427    }
428
429    /// As [`Self::wide_unsigned`].
430    fn wide_signed(value: i128) -> Self::Expr {
431        py::Expr::int(value)
432    }
433
434    /// The number, with no suffix.
435    ///
436    /// The trait's default is C's `123u`. `2u` is not a Python
437    /// expression at all — it is a `SyntaxError`, "invalid decimal
438    /// literal" — and every array bound and every string bound the
439    /// decode walk writes goes through here.
440    fn unsigned_int(value: i128) -> Self::Expr {
441        py::Expr::int(value)
442    }
443
444    /// A literal, with the two raw tokens the walk escapes through
445    /// [`Literal::Int`] rewritten.
446    ///
447    /// `emit/decode.rs`'s `literal::<T>("false")` carries a *keyword* in
448    /// an integer literal, because that variant is the tree's raw-token
449    /// escape and `false` is what C, C++, C#, Java and TypeScript all
450    /// spell a false boolean with. Python spells it `False`, and a file
451    /// carrying `zd_seen0 = false` is a `NameError` at the first
452    /// `@mutable` member — or worse, a name something else in scope
453    /// bound. Twelve sites in the decode walk reach this.
454    fn literal(value: Literal) -> Self::Expr {
455        let Literal::Int(text) = &value else {
456            return py::Expr::Literal(value);
457        };
458        match text.as_str() {
459            "true" => py::Expr::Literal(Literal::Bool(true)),
460            "false" => py::Expr::Literal(Literal::Bool(false)),
461            _ => py::Expr::Literal(value),
462        }
463    }
464
465    /// A call to a module-level function.
466    ///
467    /// `strcmp` is the one name the walk hands here that is not a
468    /// function this column generates, and it is C's. See
469    /// `strcmp_sign` for what it becomes and why.
470    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
471        if callee == "strcmp" {
472            return strcmp_sign(args);
473        }
474        py::Expr::Call {
475            func: Self::path(callee),
476            args: args.into_iter().map(py::Arg::Positional).collect(),
477        }
478    }
479
480    /// `w.write_u32(x)` — a method on the writer or the reader.
481    ///
482    /// The runtime's methods are named in the wire's convention already,
483    /// which is `snake_case`, so unlike the Java, C# and TypeScript
484    /// backends there is nothing to convert: `write_u32` is
485    /// `write_u32`. What there *is* to do is name the arguments of the
486    /// three operations whose Python signature is not in the walk's
487    /// order — see `Python::keywords_for`.
488    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
489        // `read_pid()` is the walk's name for "read a header in the
490        // MEMBER role", and in `runtime/python` that name belongs to the
491        // operation `crates/lang-python`'s decoder uses — which reads
492        // the header and opens no value block, because that column opens
493        // one itself with `begin_parameter_value`. The walk has no such
494        // pair: it reads a header and later calls `finish_pid_read`, so
495        // the block has to be opened by the read. `read_pid_as` is the
496        // entry point that does, and §7.4.1.2 makes the difference a
497        // wire question — a member's internal alignment is counted from
498        // the first octet after its header — so it is not one this
499        // backend may answer by leaving the call as it stands.
500        if method == "read_pid" && args.is_empty() {
501            return Self::runtime(
502                receiver,
503                "read_pid_as",
504                vec![Self::constant(Constant::ParameterMember)],
505            );
506        }
507        let func = Path::new(&[receiver, method]);
508        let Some(names) = Python::keywords_for(method) else {
509            return py::Expr::Call {
510                func,
511                args: args.into_iter().map(py::Arg::Positional).collect(),
512            };
513        };
514        // Only where the walk handed exactly the two the table names.
515        // A call that grew a third argument falls back to positional
516        // and becomes a `TypeError` naming the parameter, which is
517        // louder than a keyword quietly applied to the wrong slot.
518        if args.len() != names.len() {
519            return py::Expr::Call {
520                func,
521                args: args.into_iter().map(py::Arg::Positional).collect(),
522            };
523        }
524        py::Expr::Call {
525            func,
526            args: args
527                .into_iter()
528                .zip(names)
529                .map(|(value, name)| {
530                    py::Arg::Keyword(py::Keyword {
531                        name: Ident::new(name),
532                        value,
533                    })
534                })
535                .collect(),
536        }
537    }
538
539    /// `runtime/python` carries the three HDDS axes:
540    /// `begin_packed_element` / `finish_packed_element` on the writer and
541    /// the reader, the DHEADER pair for a sequence element's own length,
542    /// and `write_wstring_utf8_octets` / `read_wstring_utf8_octets`.
543    fn carries_hdds_axes() -> bool {
544        true
545    }
546
547    /// A free function the generated module imports from the runtime.
548    ///
549    /// Two of the three the walk asks for are module-level functions in
550    /// `zerodds_wire` — `no_dheader`, `utf8_octets`, `utf16_units`. The
551    /// third is not: `pid_is_user_member(header)` is
552    /// `PidRead.is_user_member()` here, a method on the header the
553    /// reader handed back, which is the same difference as C's free
554    /// function against Java's static and is decided here rather than in
555    /// the walk.
556    fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
557        if method == "pid_is_user_member" {
558            if let [py::Expr::Path(header)] = args.as_slice() {
559                let mut segments: Vec<String> = header
560                    .segments()
561                    .iter()
562                    .map(|segment| segment.as_str().to_string())
563                    .collect();
564                segments.push("is_user_member".to_string());
565                return py::Expr::Call {
566                    func: Path::new(&segments.iter().map(String::as_str).collect::<Vec<&str>>()),
567                    args: Vec::new(),
568                };
569            }
570        }
571        py::Expr::Call {
572            func: Self::runtime_name(method),
573            args: args.into_iter().map(py::Arg::Positional).collect(),
574        }
575    }
576
577    fn member(base: &str, field: &str) -> Self::Expr {
578        py::Expr::Attribute {
579            value: Box::new(py::Expr::name(base)),
580            name: Ident::new(field),
581        }
582    }
583
584    fn whole(base: &str) -> Self::Expr {
585        py::Expr::name(base)
586    }
587
588    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
589        py::Expr::Attribute {
590            value: Box::new(value),
591            name: Ident::new(field),
592        }
593    }
594
595    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
596        py::Expr::Index {
597            value: Box::new(value),
598            index: Box::new(at),
599        }
600    }
601
602    /// `len(v)`, or the bound where the materialiser fixed one.
603    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
604        match fixed {
605            Some(bound) => py::Expr::int(i128::from(bound)),
606            None => py::Expr::Call {
607                func: Path::single("len"),
608                args: vec![py::Arg::Positional(py::Expr::name(base))],
609            },
610        }
611    }
612
613    fn elements(base: &str) -> Self::Expr {
614        py::Expr::name(base)
615    }
616
617    /// `v.field` — a union's selected case member.
618    ///
619    /// `adapter/python.rs` declares a union as one dataclass with `_d`
620    /// and one `typing.Optional` field per case, so a case member is a
621    /// plain attribute. This is the Java shape and not the TypeScript
622    /// one, which needs the value narrowed to an arm before the property
623    /// exists at all.
624    fn case_member(base: &str, field: &str) -> Self::Expr {
625        Self::member(base, field)
626    }
627
628    /// `entry[0]` and `entry[1]`.
629    ///
630    /// A `map`'s entry is a two-element sequence here and not a class
631    /// with named halves: `adapter/python.rs` writes
632    /// `list[tuple[K, V]]`, and §7.4.1.1.4's "sequence of entries" is
633    /// what the wire form is. The index and not a name because there is
634    /// no name to use.
635    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
636        py::Expr::Index {
637            value: Box::new(entry),
638            index: Box::new(py::Expr::int(i128::from(!key))),
639        }
640    }
641
642    fn absent() -> Self::Expr {
643        py::Expr::Literal(Literal::Null)
644    }
645
646    /// Nothing.
647    ///
648    /// The guard this asks for is C's: `zdw_read_string_alloc` answers
649    /// `NULL` where the reader's arena is exhausted, and every use of
650    /// what it produced is tested against that. A Python `str` owns its
651    /// own storage and there is no such state, so the guard is not
652    /// written at all rather than written against a value that is always
653    /// there.
654    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
655        None
656    }
657
658    /// The member itself. `typing.Optional[T]` is the value or `None`,
659    /// with no wrapper to unwrap the way C#'s `Nullable<T>` has.
660    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
661        Self::member(base, field)
662    }
663
664    /// `v.field is not None`.
665    ///
666    /// `is not` and not `!=`. `!=` calls `__eq__`, which a generated
667    /// dataclass defines over its fields, so `v.field != None` is a
668    /// structural comparison against `None` rather than an identity
669    /// test — and for a dataclass whose `__eq__` returns
670    /// `NotImplemented` against a non-dataclass, Python falls back to
671    /// identity and the two agree only by accident. PEP 8 says the same
672    /// thing: "Comparisons to singletons like None should always be done
673    /// with `is` or `is not`."
674    fn present(base: &str, field: &str) -> Self::Expr {
675        py::Expr::Binary {
676            op: "is not",
677            lhs: Box::new(Self::member(base, field)),
678            rhs: Box::new(Self::absent()),
679        }
680    }
681
682    /// Nothing on the way in, `v.field = None` on the way out.
683    ///
684    /// Writing the member is what makes it present — the field's
685    /// declared default is `None` and the reader starts from Table 9's
686    /// sample — so the present case has no statement to add.
687    /// `None` in every case field: `adapter/python.rs` declares them
688    /// `typing.Optional[...] = None`, and the class promises that at most
689    /// one is live.
690    fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
691        fields
692            .iter()
693            .map(|field| py::Statement::Set {
694                place: Self::member(base, field),
695                value: Self::absent(),
696            })
697            .collect()
698    }
699
700    /// The default's rows as lists, outermost first: `v = list(v)`, then
701    /// `v[zd_i0] = list(v[zd_i0])` inside a loop per further dimension.
702    ///
703    /// `adapter/python.rs` declares an array member `tuple[T, …]` and
704    /// `default_python.rs` answers a tuple to match, which the reader
705    /// cannot assign an element into; [`Self::close_array`] turns every
706    /// row back once the elements are in.
707    fn open_array(base: &str, lengths: &[i128]) -> Vec<Self::Stmt> {
708        array_rows(base, lengths, 0, false)
709    }
710
711    /// Every row a tuple again, innermost first, so what the reader
712    /// answers with is the `tuple[T, …]` the member declares. A list in
713    /// its place compares unequal to the tuple a caller wrote —
714    /// `(1, 2) == [1, 2]` is `False` — and a sample read back from the
715    /// wire was not `==` to the one written (measured:
716    /// `idl-conformance/python`, `combo::Telemetry.window`).
717    fn close_array(base: &str, lengths: &[i128]) -> Vec<Self::Stmt> {
718        array_rows(base, lengths, 0, true)
719    }
720
721    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
722        if present {
723            return Vec::new();
724        }
725        vec![py::Statement::Set {
726            place: Self::member(base, field),
727            value: Self::absent(),
728        }]
729    }
730
731    /// The member, with nothing to do first.
732    ///
733    /// `adapter/python.rs`'s optional member is a `typing.Optional[T]`
734    /// field that already exists on the dataclass, so unlike C++'s
735    /// `std::optional` there is nothing to engage before there is
736    /// somewhere to write.
737    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
738        (Vec::new(), Self::member(base, field))
739    }
740
741    /// The operators, in Python's spelling.
742    ///
743    /// `&&` and `||` are `and` and `or`, which are not the same
744    /// operators as `&` and `|`: the bitwise pair does not short-circuit
745    /// and returns an integer. The walk uses both — `status_is_ok() &&
746    /// reader_position() < zd_end` needs the short circuit, and a
747    /// bitmask's undeclared-bit test needs the bitwise `&` — and mapping
748    /// them onto one would break whichever it was not.
749    ///
750    /// `=` is left as it is and never reaches a rendered expression:
751    /// [`Self::perform`] is the only thing that receives one and turns
752    /// it into a statement.
753    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
754        py::Expr::Binary {
755            op: match op {
756                "&&" => "and",
757                "||" => "or",
758                other => other,
759            },
760            lhs: Box::new(lhs),
761            rhs: Box::new(rhs),
762        }
763    }
764
765    /// `not a` and `~a`.
766    ///
767    /// `not` carries its trailing space because `notx` is an identifier;
768    /// `~` takes none. The tree records which, so the renderer does not
769    /// have to guess.
770    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
771        py::Expr::Unary {
772            op: match op {
773                "!" => "not ",
774                other => other,
775            },
776            value: Box::new(value),
777        }
778    }
779
780    fn paren(value: Self::Expr) -> Self::Expr {
781        py::Expr::Paren(Box::new(value))
782    }
783
784    /// The identity on every integer, and a rewrite on three things that
785    /// are not integers.
786    ///
787    /// **Why there is no narrowing.** A Python `int` holds every IDL
788    /// width exactly, so `(long) x` and `static_cast<int32_t>(x)` have no
789    /// counterpart: there is nothing the value could have lost. The
790    /// runtime range-checks what it writes — `Writer._check` — so a value
791    /// outside the type raises at the octet rather than being folded
792    /// silently, which is the opposite trade from a C cast and is the
793    /// one this column wants.
794    ///
795    /// **Three rewrites, all at a union's discriminator.** The walk
796    /// writes `cast(value_type(discriminator), wide_int(label))` for
797    /// every case label, and the label arrives as an integer:
798    ///
799    /// - a `char` or `wchar` discriminator holds a `str`, and
800    ///   `v._d == 97` is `False` for every sample. The label is the code
801    ///   point and the member is the character, so the literal becomes
802    ///   one.
803    /// - a `boolean` discriminator holds a `bool`. `v._d == 1` happens
804    ///   to be true in Python — `True == 1` — but `False` is what the
805    ///   sample carries and `True`/`False` is what says so.
806    /// - `signed char` is not a Python type at all; see `SIGNED_CHAR`
807    ///   and `strcmp_sign`.
808    ///
809    /// An enumeration discriminator needs none of it: `adapter/python.rs`
810    /// declares an `enum.IntEnum`, whose members *are* `int`s, so the
811    /// label compares equal to the enumerator without either side moving.
812    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
813        if type_ == py::Type::name(SIGNED_CHAR) {
814            return signed_char(value);
815        }
816        let py::Expr::Literal(Literal::Int(text)) = &value else {
817            return value;
818        };
819        let digits = text.strip_prefix('-').unwrap_or(text);
820        if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
821            return value;
822        }
823        if type_ == py::Type::name("str") {
824            // `chr(<code>)` and not a character literal: `Literal::Char`
825            // carries a Rust `char`, which is a Unicode scalar value and
826            // rejects `0xD800..=0xDFFF`, while a Python `str` holds any
827            // code point and §7.4.1.1.1 writes a `wchar` as one UTF-16
828            // code unit — a lone surrogate among them.
829            //
830            // Going through `char::from_u32` left the *integer* standing
831            // where a one-character `str` belongs, and Python compares
832            // an `int` to a `str` as unequal without a word: a `wchar`
833            // discriminator whose case label is a surrogate would select
834            // no branch, every time, silently. The frontend admits that
835            // label, because 0xD800 is inside a `wchar`'s range.
836            return text
837                .parse::<u32>()
838                .ok()
839                .filter(|code| *code <= u32::from(u16::MAX))
840                .map_or_else(
841                    || value.clone(),
842                    |code| match char::from_u32(code) {
843                        // The character itself where Rust can spell it,
844                        // so `case 'a'` stays `"a"` in the generated
845                        // file and reads as the label it came from.
846                        Some(point) => py::Expr::Literal(Literal::Char(point)),
847                        // `chr(..)` for the surrogates, which are code
848                        // units a `wchar` carries and Rust's `char` will
849                        // not hold.
850                        None => py::Expr::Call {
851                            func: Path::single("chr"),
852                            args: vec![py::Arg::Positional(py::Expr::int(i128::from(code)))],
853                        },
854                    },
855                );
856        }
857        if type_ == py::Type::name("bool") {
858            return py::Expr::Literal(Literal::Bool(text != "0"));
859        }
860        value
861    }
862
863    /// The value, unchanged.
864    ///
865    /// [`Handoff`] abstracts C's `&v->x` against `v.x`. Python passes
866    /// every object by reference and takes no address, so both forms are
867    /// the value.
868    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
869        value
870    }
871
872    // -- statements ----------------------------------------------------
873
874    /// `name: T = value`, or `name = value` where there is nothing to
875    /// initialise it with.
876    ///
877    /// The annotation is written because it costs nothing and says the
878    /// width the Python type does not: a reader of the generated file
879    /// sees `zd_n: int = r.read_u32()` and knows what the local is for.
880    /// An uninitialised local is a **binding with no value**, which
881    /// Python does have — `zd_h: EmHeaderRead` declares the annotation
882    /// and binds nothing — and that is exactly what the walk means when
883    /// it declares `zd_h` and assigns it inside the loop.
884    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
885        py::Statement::Annotated(py::Annotated {
886            doc: Doc::none(),
887            name: Ident::new(name),
888            type_,
889            value,
890        })
891    }
892
893    /// An expression evaluated for its effect — or, where the walk built
894    /// an assignment, the assignment.
895    ///
896    /// `a = b` is not an expression in Python and `(a = b)` is a
897    /// `SyntaxError`. The shared walk's `assign` helper and its
898    /// `increment` both build `T::binary("=", …)` and hand it here, and
899    /// this is the only place either can arrive: nothing else in
900    /// `emit/encode.rs` or `emit/decode.rs` puts an `=` anywhere but
901    /// directly inside a `T::perform`. So the shape is recognised here
902    /// and nowhere else, and an `=` that escaped into a nested
903    /// expression would be a `SyntaxError` in the generated file naming
904    /// the line — which is louder than a walrus quietly standing in for
905    /// it.
906    fn perform(value: Self::Expr) -> Self::Stmt {
907        if let py::Expr::Binary { op: "=", lhs, rhs } = value {
908            return py::Statement::Set {
909                place: *lhs,
910                value: *rhs,
911            };
912        }
913        py::Statement::Expr(value)
914    }
915
916    /// `return v`.
917    ///
918    /// Every `T::give_back` the decode walk builds is `give_back(None)`,
919    /// which is C's shape: a `void` decoder that wrote through a
920    /// pointer. This column's decoders answer with the sample — see
921    /// [`Self::routine`] and [`Self::read_call`] — so a bare `return`
922    /// would answer `None` and the caller would assign it. On a refusal
923    /// the partially filled `v` goes back, exactly as the TypeScript
924    /// column does, and the caller reads `reader_status()` to find out
925    /// what it is.
926    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
927        py::Statement::Return(Some(value.unwrap_or_else(|| py::Expr::name("v"))))
928    }
929
930    fn when(
931        condition: Self::Expr,
932        then: Self::Block,
933        otherwise: Option<Self::Block>,
934    ) -> Self::Stmt {
935        py::Statement::If {
936            condition,
937            then: Self::suite(then),
938            // An `else:` the walk built empty is dropped rather than
939            // padded: `else: pass` says nothing and reads as though a
940            // branch had been forgotten.
941            otherwise: otherwise.filter(|body| !body.is_empty()),
942        }
943    }
944
945    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
946        py::Statement::For {
947            counter: Ident::new(counter),
948            bound,
949            body: Self::suite(body),
950        }
951    }
952
953    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
954        py::Statement::While {
955            condition,
956            body: Self::suite(body),
957        }
958    }
959
960    fn leave() -> Self::Stmt {
961        py::Statement::Break
962    }
963
964    fn note(doc: Doc) -> Self::Stmt {
965        py::Statement::Comment(doc)
966    }
967
968    /// The call, and nothing around it.
969    ///
970    /// The trait's default is `(void) expr;`, which is C's way of saying
971    /// an ignored return was meant. Python has no such diagnostic and
972    /// `(None) expr` is not a statement at all, so the call stands
973    /// alone.
974    fn drop_value(value: Self::Expr) -> Self::Stmt {
975        py::Statement::Expr(value)
976    }
977
978    /// Nothing.
979    ///
980    /// `-Wunused-parameter -Werror` is what this exists for and Python
981    /// has no counterpart; `zd_tc` on a line of its own is a legal
982    /// statement that does nothing, which is worse than no statement.
983    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
984        Vec::new()
985    }
986
987    /// `place = zd_dec_T(r, zd_tc)`.
988    ///
989    /// The trait's default hands the destination *to* the callee, which
990    /// is C's shape: its decoder takes a `T *` and fills it. A Python
991    /// function has no out-parameter — rebinding a parameter rebinds the
992    /// local and nothing else — so this column's decoders answer with
993    /// the value and the destination is assigned at the call site.
994    fn read_call(
995        callee: &str,
996        place: Self::Expr,
997        _form: Handoff,
998        behaviour: Self::Expr,
999    ) -> Self::Stmt {
1000        py::Statement::Set {
1001            place,
1002            value: Self::call(callee, vec![py::Expr::name("r"), behaviour]),
1003        }
1004    }
1005
1006    /// `v = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on.
1007    fn read_forward(callee: &str) -> Self::Stmt {
1008        Self::read_call(
1009            callee,
1010            py::Expr::name("v"),
1011            Handoff::Value,
1012            py::Expr::name("zd_tc"),
1013        )
1014    }
1015
1016    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1017        statements
1018    }
1019
1020    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1021        block
1022    }
1023
1024    /// Room for `count` values, and the fill inside it.
1025    ///
1026    /// Two shapes, and the difference is `assigned_whole` — which is the
1027    /// difference between a slot that *is* storage and a slot that has
1028    /// to be one before anything can be written into it.
1029    ///
1030    /// **A sequence** (`assigned_whole`): `v = [None] * zd_n`, because
1031    /// the element decoder assigns the slot outright. `[None] * n` and
1032    /// not `[[..]] * n`: `None` is immutable and repeating it aliases
1033    /// nothing, which is not true of the map's shape below.
1034    ///
1035    /// **A `map`** (not `assigned_whole`): each entry is written half at
1036    /// a time — `v[i][0] = …`, `v[i][1] = …` — so each slot has to be a
1037    /// *distinct* mutable pair. `[[None, None]] * n` would be one list
1038    /// repeated `n` times and every entry would be the last one read;
1039    /// the loop that appends is what makes them distinct. Once both
1040    /// halves are there the run is turned back into tuples, so what the
1041    /// decoder answers with is what `adapter/python.rs` declared —
1042    /// `list[tuple[K, V]]` — and what `from_json` built.
1043    fn make_room(
1044        base: &str,
1045        _element: Self::Type,
1046        count: Self::Expr,
1047        assigned_whole: bool,
1048        fill: Vec<Self::Stmt>,
1049    ) -> Vec<Self::Stmt> {
1050        let slot = |index: py::Expr| py::Expr::Index {
1051            value: Box::new(py::Expr::name(base)),
1052            index: Box::new(index),
1053        };
1054        let mut out = Vec::new();
1055        if assigned_whole {
1056            out.push(py::Statement::Set {
1057                place: py::Expr::name(base),
1058                value: py::Expr::Binary {
1059                    op: "*",
1060                    lhs: Box::new(py::Expr::List(vec![py::Expr::Literal(Literal::Null)])),
1061                    rhs: Box::new(count.clone()),
1062                },
1063            });
1064            out.extend(fill);
1065            return out;
1066        }
1067        out.push(py::Statement::Set {
1068            place: py::Expr::name(base),
1069            value: py::Expr::List(Vec::new()),
1070        });
1071        out.push(py::Statement::For {
1072            counter: Ident::new(ENTRY_INDEX),
1073            bound: count.clone(),
1074            body: vec![py::Statement::Expr(py::Expr::Call {
1075                func: Path::new(&[base, "append"]),
1076                args: vec![py::Arg::Positional(py::Expr::List(vec![
1077                    py::Expr::Literal(Literal::Null),
1078                    py::Expr::Literal(Literal::Null),
1079                ]))],
1080            })],
1081        });
1082        out.extend(fill);
1083        out.push(py::Statement::For {
1084            counter: Ident::new(ENTRY_INDEX),
1085            bound: count,
1086            body: vec![py::Statement::Set {
1087                place: slot(py::Expr::name(ENTRY_INDEX)),
1088                value: py::Expr::Call {
1089                    func: Path::single("tuple"),
1090                    args: vec![py::Arg::Positional(slot(py::Expr::name(ENTRY_INDEX)))],
1091                },
1092            }],
1093        });
1094        out
1095    }
1096
1097    /// `v = v[:count]`.
1098    ///
1099    /// §7.3.1.2.1.12.3's TRIM for a bounded collection, applied after
1100    /// every element has been read: stopping at the bound would leave
1101    /// the rest of the sequence in the stream and put every following
1102    /// member at the wrong offset.
1103    ///
1104    /// A slice and not `del v[count:]`, which does the same thing to a
1105    /// `list` and not to a `tuple` — and the walk reaches this for a
1106    /// bounded sequence, whose Python holder `adapter/python.rs` is free
1107    /// to change.
1108    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
1109        vec![py::Statement::Set {
1110            place: py::Expr::name(base),
1111            value: py::Expr::Index {
1112                value: Box::new(py::Expr::name(base)),
1113                index: Box::new(py::Expr::Slice {
1114                    start: None,
1115                    end: Some(Box::new(count)),
1116                }),
1117            },
1118        }]
1119    }
1120
1121    /// The subscript, with nothing before or after it.
1122    ///
1123    /// The seam exists for `std::vector<bool>`, whose `operator[]`
1124    /// answers a proxy. A Python `list` element is an ordinary
1125    /// reference at every element type.
1126    fn sequence_element(
1127        base: &str,
1128        index: Self::Expr,
1129        _element: Self::Type,
1130    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1131        (
1132            Vec::new(),
1133            Self::index(Self::elements(base), index),
1134            Vec::new(),
1135        )
1136    }
1137
1138    /// A local the body writes *into*.
1139    ///
1140    /// One caller: `zd_scratch`, the entry of an uncounted `map`, which
1141    /// the reader fills a half at a time. There is only one type this is
1142    /// ever asked for — see [`Self::map_entry_name`] — and it is a
1143    /// two-slot list, so this answers with one rather than with a
1144    /// constructor call to a class this column does not declare.
1145    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
1146        if type_ == py::Type::name(MAP_ENTRY) {
1147            return py::Statement::Set {
1148                place: py::Expr::name(name),
1149                value: py::Expr::List(vec![
1150                    py::Expr::Literal(Literal::Null),
1151                    py::Expr::Literal(Literal::Null),
1152                ]),
1153            };
1154        }
1155        Self::declare(type_, name, None)
1156    }
1157
1158    // -- assembly ------------------------------------------------------
1159
1160    /// `def zd_enc_T(w, v) -> None:` and `def zd_dec_T(r, zd_tc) -> T:`.
1161    ///
1162    /// A forward declaration is answered with nothing: Python resolves a
1163    /// global at the call and not at the `def`, so a recursive or
1164    /// mutually recursive type needs no declaration ahead of its body.
1165    /// That is what `body: None` asks for and `None` is the honest
1166    /// answer — the caller emits nothing.
1167    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1168        let body = spec.body?;
1169        let mut params = vec![py::Param {
1170            name: Ident::new(if spec.writing { "w" } else { "r" }),
1171            type_: Some(py::Type::name(if spec.writing {
1172                "Writer"
1173            } else {
1174                "Reader"
1175            })),
1176            default: None,
1177        }];
1178        if spec.writing {
1179            params.push(py::Param {
1180                name: Ident::new("v"),
1181                type_: Some(spec.sample.clone()),
1182                default: None,
1183            });
1184        }
1185        params.extend(spec.extra.into_iter().map(|(type_, name)| py::Param {
1186            name: Ident::new(&name),
1187            type_: Some(type_),
1188            default: None,
1189        }));
1190        let (returns, body) = if spec.writing {
1191            (Self::nothing(), body)
1192        } else {
1193            (spec.sample, Self::reading_body(&spec.name, body))
1194        };
1195        Some(py::Statement::Function(py::Function {
1196            doc: spec.doc,
1197            decorators: Vec::new(),
1198            name: Ident::new(&spec.name),
1199            params,
1200            returns: Some(returns),
1201            body: Self::suite(body),
1202        }))
1203    }
1204
1205    fn initialized_reader(spec: Routine<Self>, initializer: Self::Stmt) -> Option<Self::Decl> {
1206        if spec.writing {
1207            return None;
1208        }
1209        let mut body = vec![initializer];
1210        body.extend(spec.body?);
1211        body.push(py::Statement::Return(Some(py::Expr::name("v"))));
1212        let mut params = vec![py::Param {
1213            name: Ident::new("r"),
1214            type_: Some(py::Type::name("Reader")),
1215            default: None,
1216        }];
1217        params.extend(spec.extra.into_iter().map(|(type_, name)| py::Param {
1218            name: Ident::new(&name),
1219            type_: Some(type_),
1220            default: None,
1221        }));
1222        Some(py::Statement::Function(py::Function {
1223            doc: spec.doc,
1224            decorators: Vec::new(),
1225            name: Ident::new(&spec.name),
1226            params,
1227            returns: Some(spec.sample),
1228            body: Self::suite(body),
1229        }))
1230    }
1231
1232    /// A call to a generated defaulter and not a literal in place.
1233    ///
1234    /// A literal cannot carry an aggregate, and one written at every
1235    /// position a member can go missing would put the same answer in the
1236    /// generated file as many times as the type is reached — where two
1237    /// copies can differ. [`crate::default_python`] carries the whole
1238    /// table and the argument.
1239    ///
1240    /// # Errors
1241    /// Never; the signature matches the emitters around it.
1242    fn default_into(
1243        ast: &Ast,
1244        id: Id<TypeNode>,
1245        place: Self::Expr,
1246    ) -> Result<Self::Stmt, EncodeError> {
1247        Ok(crate::default_python::default_into(ast, id, place))
1248    }
1249
1250    /// # Errors
1251    /// As [`crate::default_python::member_default`]: a `@default` this
1252    /// column cannot spell as a Python expression is refused rather than
1253    /// silently replaced by Table 9's zero.
1254    fn member_default(
1255        ast: &Ast,
1256        node: &MemberNode,
1257        type_: Id<TypeNode>,
1258        place: Self::Expr,
1259    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1260        crate::default_python::member_default(ast, node, type_, place)
1261    }
1262
1263    /// `a.b.Colour.RED`, written from the module root.
1264    ///
1265    /// The trait's default answers the bare enumerator name, which is
1266    /// right for a target that flattens its modules into a prefix and
1267    /// wrong here: `adapter/python.rs` nests each IDL module in a class
1268    /// and an `enum.IntEnum` inside it, and a generated codec sits at the
1269    /// module root — where `RED` resolves to nothing, or to whatever
1270    /// else happened to be bound.
1271    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1272        let Ok(path) = declared_path(ast, id) else {
1273            return Self::name(&escape_name(enumerator));
1274        };
1275        let mut segments: Vec<String> = path
1276            .segments()
1277            .iter()
1278            .map(|segment| segment.as_str().to_string())
1279            .collect();
1280        segments.push(escape_name(enumerator));
1281        py::Expr::Path(Path::new(
1282            &segments.iter().map(String::as_str).collect::<Vec<&str>>(),
1283        ))
1284    }
1285}
1286
1287/// The counter the two entry loops of [`Python::make_room`] run on.
1288///
1289/// Its own name and not the walk's `zd_i`: the fill the walk handed over
1290/// declares `zd_i` itself, and a loop of this file's that reused it
1291/// would leave `zd_i` bound to the wrong number after the fill — Python
1292/// has no block scope, so a `for` counter outlives its loop.
1293const ENTRY_INDEX: &str = "zd_e";
1294
1295/// What [`CodecTarget::map_entry_name`] answers with.
1296///
1297/// Not a type this column declares. It is a marker the two seams that
1298/// receive it — [`Python::make_room`] and [`Python::storage`] — match
1299/// on, because a `map` entry has no Python class and both of them need
1300/// to know that the thing they are making room for is a two-slot list.
1301const MAP_ENTRY: &str = "_zd_map_entry";
1302
1303/// A character's code point read as C's signed `char`.
1304///
1305/// `emit/encode.rs`'s `map_key_less` writes
1306/// `T::cast(T::named("signed char"), key)` where a `map`'s key is an IDL
1307/// `char` and the entry order is `MapKeyOrdering::SignedInteger` — which
1308/// `map-key-char` in the corpus reaches. The comparison "says the
1309/// signedness out loud rather than taking the C type's", because an IDL
1310/// `char` is a C `char` and its signedness is implementation-defined:
1311/// the corpus was recorded on a host where it is signed, so `U+00FF`
1312/// sorts *below* `U+0000` and a column comparing code points would order
1313/// the entries differently.
1314///
1315/// `((ord(k) + 128) % 256) - 128` is the whole of it: the two's
1316/// complement reading of one octet. `ord("\x00")` gives 0, `ord("\x7f")`
1317/// 127, `ord("\x80")` −128 and `ord("\xff")` −1.
1318///
1319/// Arithmetic and not a helper in `runtime/python`. A conditional
1320/// expression would read more plainly and the tree has no node for one;
1321/// a helper would put a *language mapping* question — how wide and how
1322/// signed C spells a character — into a runtime whose subject is §7.4,
1323/// where a second column could then reach it and inherit this column's
1324/// host.
1325fn signed_char(value: py::Expr) -> py::Expr {
1326    let point = py::Expr::Call {
1327        func: Path::single("ord"),
1328        args: vec![py::Arg::Positional(value)],
1329    };
1330    let wrapped = py::Expr::Binary {
1331        op: "%",
1332        lhs: Box::new(py::Expr::Binary {
1333            op: "+",
1334            lhs: Box::new(point),
1335            rhs: Box::new(py::Expr::int(128)),
1336        }),
1337        rhs: Box::new(py::Expr::int(256)),
1338    };
1339    py::Expr::Binary {
1340        op: "-",
1341        lhs: Box::new(wrapped),
1342        rhs: Box::new(py::Expr::int(128)),
1343    }
1344}
1345
1346/// `strcmp(a, b)` as its sign, computed in Python.
1347///
1348/// `emit/encode.rs`'s `map_key_less` compares two string keys with
1349/// `strcmp(..) < 0` under `MapKeyOrdering::CodeUnits`. C11 §7.24.4 says
1350/// `strcmp` compares as `unsigned char`; Python's `str` comparison is by
1351/// code point, and UTF-8 is order preserving, so the two orders are the
1352/// same order and `(a > b) - (a < b)` is the same sign.
1353///
1354/// Written as an expression rather than added to `runtime/python`
1355/// deliberately: this is not a wire question and nothing about it
1356/// belongs in a runtime that implements §7.4. A helper there would be a
1357/// second place a column's key order could be decided.
1358fn strcmp_sign(args: Vec<py::Expr>) -> py::Expr {
1359    let [left, right] = <[py::Expr; 2]>::try_from(args).unwrap_or_else(|args| {
1360        // Unreachable for the one call site, which passes exactly two.
1361        // A different arity would be a walk that changed under this
1362        // file, and answering `0` would say the two keys are equal —
1363        // which reorders a `map` silently. `len(..)` of the arguments is
1364        // not something Python can be asked here, so the shape that
1365        // survives is a call to a name no module binds: a `NameError`
1366        // that says `strcmp` at the line it happened.
1367        [
1368            py::Expr::Call {
1369                func: Path::single("strcmp"),
1370                args: args.into_iter().map(py::Arg::Positional).collect(),
1371            },
1372            py::Expr::int(0),
1373        ]
1374    });
1375    let side = |op: &'static str| {
1376        py::Expr::Paren(Box::new(py::Expr::Binary {
1377            op,
1378            lhs: Box::new(left.clone()),
1379            rhs: Box::new(right.clone()),
1380        }))
1381    };
1382    py::Expr::Paren(Box::new(py::Expr::Binary {
1383        op: "-",
1384        lhs: Box::new(side(">")),
1385        rhs: Box::new(side("<")),
1386    }))
1387}
1388
1389/// The Python type of a value, structurally.
1390///
1391/// The counterpart of `adapter::python::python_member_type`, which
1392/// spells a *member's* declared type and carries the width in
1393/// `typing.Annotated` metadata a codec has no use for. Both read the
1394/// same model and agree on every shape.
1395///
1396/// Every named answer is the path from the **module root** and not the
1397/// declaration's own name. `adapter/python.rs` nests an IDL module in a
1398/// class and a generated codec sits at the root, so `Point` there names
1399/// nothing; `module_prefix` answers the empty string because a target
1400/// with scopes has no prefix to add, and that is not the same as having
1401/// nothing to qualify.
1402///
1403/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1404fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> py::Type {
1405    use zerodds_ast::{PrimitiveKind, TypeKind};
1406    // `object` and not `typing.Any`: a value of `Any` may be used as
1407    // anything without a checker saying so, and a shape this column
1408    // could not spell is exactly the case that should not pass quietly.
1409    let unknown = || py::Type::name("object");
1410    if depth > MAX_TYPE_DEPTH {
1411        return unknown();
1412    }
1413    let Ok(node) = ast.type_(id) else {
1414        return unknown();
1415    };
1416    let declared = || declared_path(ast, id).map_or_else(|_| unknown(), py::Type::Name);
1417    let inner = |half: Option<Id<TypeNode>>| {
1418        half.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1))
1419    };
1420    match &node.kind {
1421        TypeKind::Primitive(kind) => match kind {
1422            PrimitiveKind::Boolean => Python::scalar(Scalar::Bool),
1423            PrimitiveKind::Char | PrimitiveKind::WChar => Python::scalar(Scalar::Char),
1424            PrimitiveKind::Float32 | PrimitiveKind::Float64 => Python::scalar(Scalar::Double),
1425            PrimitiveKind::Float128 => Python::scalar(Scalar::LongDouble),
1426            _ => Python::scalar(Scalar::Int32),
1427        },
1428        // A `fixed<P,S>` is a decimal *string* and not a `float` or a
1429        // `decimal.Decimal`: `fixed-scales` declares `fixed<31,10>` and
1430        // its value has thirty significant digits, which neither holds,
1431        // and `FixedDigitCount::ValuePrecision` measures how many digits
1432        // a peer writes — a holder that normalises leading zeros away
1433        // has already answered that.
1434        TypeKind::String { .. } | TypeKind::Fixed { .. } => py::Type::name("str"),
1435        // An enumeration is an `enum.IntEnum` and a bitmask an
1436        // `enum.IntFlag`; the declared name and not `int`, because only
1437        // one of the two says which enumeration.
1438        TypeKind::Enumeration { .. }
1439        | TypeKind::Bitmask { .. }
1440        | TypeKind::Bitset { .. }
1441        | TypeKind::Structure { .. }
1442        | TypeKind::Union { .. }
1443        | TypeKind::Valuetype { .. }
1444        | TypeKind::Native => declared(),
1445        TypeKind::Sequence { element, .. } => {
1446            py::Type::subscript(Path::single("list"), vec![inner(element.resolved())])
1447        }
1448        // The `tuple[T, …]` the materialiser writes for a member, one
1449        // argument per element and one `tuple[..]` per dimension:
1450        // `long a[2][3]` is `tuple[tuple[int, int, int], tuple[int, int,
1451        // int]]`. The reader builds the rows as lists between
1452        // `open_array` and `close_array` and answers with tuples.
1453        TypeKind::Array { element, dims } => {
1454            let mut out = inner(element.resolved());
1455            for dim in dims.iter().rev() {
1456                let Some(length) = ast
1457                    .eval_integral(*dim)
1458                    .ok()
1459                    .and_then(|length| usize::try_from(length).ok())
1460                else {
1461                    return unknown();
1462                };
1463                out = py::Type::subscript(Path::single("tuple"), vec![out; length]);
1464            }
1465            out
1466        }
1467        // §7.4.1.1.4: a sequence of entries, and the entry is a pair.
1468        TypeKind::Map { key, value, .. } => py::Type::subscript(
1469            Path::single("list"),
1470            vec![py::Type::subscript(
1471                Path::single("tuple"),
1472                vec![inner(key.resolved()), inner(value.resolved())],
1473            )],
1474        ),
1475        // A `typedef` has no Python declaration at all —
1476        // `adapter/python.rs` writes a comment where the other columns
1477        // write an alias, because a Python alias is evaluated eagerly
1478        // and could not be ordered against the classes it names — so a
1479        // codec resolves through it.
1480        TypeKind::Alias { aliased } => inner(aliased.resolved()),
1481        // A boxed value is a reference, absent when it is not there.
1482        TypeKind::ValueBox { boxed } => py::Type::Optional(Box::new(inner(boxed.resolved()))),
1483        // No Python data form at all; `adapter/python.rs` refuses a
1484        // member carrying one by name.
1485        TypeKind::Any
1486        | TypeKind::ValueBase
1487        | TypeKind::ObjectRef { .. }
1488        | TypeKind::AnnotationType { .. } => unknown(),
1489    }
1490}
1491
1492/// `v`, `v[zd_i0]`, `v[zd_i0][zd_i1]`, … — an array's row `depth`
1493/// indices in, spelled with the counters `array_body` loops over.
1494fn array_row(base: &str, depth: usize) -> py::Expr {
1495    let mut out = py::Expr::name(base);
1496    for level in 0..depth {
1497        out = py::Expr::Index {
1498            value: Box::new(out),
1499            index: Box::new(py::Expr::name(&format!("zd_i{level}"))),
1500        };
1501    }
1502    out
1503}
1504
1505/// Every row of an array from `depth` inward passed through `list`,
1506/// top-down so a row is a list before its own rows are reached, or —
1507/// `into_tuple` — through `tuple`, bottom-up so a row's rows are tuples
1508/// before it is.
1509///
1510/// Built from the innermost row outward, each level wrapping the loop
1511/// over the level inside it. IDL puts no bound on the number of an
1512/// array's dimensions, so a version that recursed once per dimension
1513/// could be driven as deep as the file asked.
1514fn array_rows(base: &str, lengths: &[i128], depth: usize, into_tuple: bool) -> Vec<py::Statement> {
1515    let func = if into_tuple { "tuple" } else { "list" };
1516    let convert = |level: usize| {
1517        let row = array_row(base, level);
1518        py::Statement::Set {
1519            place: row.clone(),
1520            value: py::Expr::Call {
1521                func: Path::single(func),
1522                args: vec![py::Arg::Positional(row)],
1523            },
1524        }
1525    };
1526    let innermost = lengths.len().saturating_sub(1).max(depth);
1527    let mut rows = vec![convert(innermost)];
1528    for level in (depth..innermost).rev() {
1529        let inward = py::Statement::For {
1530            counter: Ident::new(&format!("zd_i{level}")),
1531            bound: py::Expr::int(lengths[level]),
1532            body: Python::suite(rows),
1533        };
1534        rows = if into_tuple {
1535            vec![inward, convert(level)]
1536        } else {
1537            vec![convert(level), inward]
1538        };
1539    }
1540    rows
1541}