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}