zerodds_lang_cst/python/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Python concrete syntax tree. Spec §1.
4//!
5//! Python has no braces, no declarations and no type syntax of its own —
6//! an annotation is an ordinary expression. The tree therefore differs
7//! from the other four in three ways, and each is Python's rather than a
8//! simplification:
9//!
10//! 1. **A block is a list of statements and nothing else.** There is no
11//! delimiter to get wrong, so [`Class`] carries a `body` and the
12//! renderer owns the indentation. A body that renders to nothing is
13//! not legal Python, which is why [`Statement::Pass`] exists as a node
14//! a rule emits rather than something the renderer inserts.
15//! 2. **[`Type`] is separate from [`Expr`] although Python spells them
16//! the same.** Keeping them apart is what stops a rule building
17//! `Optional[a, b]` or `list[1]`: the tree can only hold the forms an
18//! annotation actually takes. Where an annotation genuinely needs a
19//! value — the metadata of a [`Type::Annotated`] — an [`Expr`] appears
20//! inside the type, which is exactly what `typing.Annotated` is for.
21//! 3. **There is no visibility and no modifier.** Python has neither, so
22//! [`crate::common::Visibility`] is not used here; a leading
23//! underscore is a convention and is part of the identifier.
24//!
25//! ## What is here, and what the second pass added
26//!
27//! The tree started as the *types* an IDL file becomes and nothing else.
28//! The wire codecs are the second pass, and they are what
29//! [`Function`], [`Statement::If`], [`Statement::For`],
30//! [`Statement::While`], [`Statement::Break`], [`Statement::Return`],
31//! [`Statement::Set`], [`Expr::Attribute`], [`Expr::Index`],
32//! [`Expr::Unary`], [`Expr::Paren`] and [`Expr::List`] arrived for:
33//! `crates/lang-rules/src/emit/python.rs` builds every one of them out
34//! of the shared walk, and each has a renderer test in
35//! `zerodds_lang_render::python`.
36//!
37//! What is here is still bounded by that rule in the other direction —
38//! every form has a renderer test, so no arm of
39//! `zerodds_lang_render::python` is code that only claims to work. A
40//! field or a variant with no test and no producer would be exactly
41//! that, which is why [`Class`] has no `metaclass=` and [`Function`] has
42//! no `*args`, no `**kwargs` and no `async`.
43//!
44//! ## A binding and a store are two statements, not one
45//!
46//! Python spells both `x = y`. They are apart here — [`Statement::Assign`]
47//! binds a *name* and [`Statement::Set`] writes into a *place* — because
48//! only one of them can be either. `RED = 0` inside an `enum.IntEnum` is
49//! a member and has to be a name; `v.field = x` and `v[i] = x` are the
50//! codec's, and neither is a name a scope binds. A single node taking an
51//! arbitrary target would let a materialiser declare an enumeration
52//! member called `v[0]`, and nothing between it and the file would
53//! notice.
54
55use crate::common::{Doc, Ident, Literal, Path};
56use alloc::boxed::Box;
57use alloc::string::{String, ToString};
58use alloc::vec::Vec;
59
60/// A Python source file.
61///
62/// One module per IDL file. Python's own unit of scope is the module, so
63/// unlike the Java tree there is no outer type everything nests inside:
64/// a top-level IDL declaration is a top-level Python statement.
65#[derive(Debug, Clone, Default, PartialEq, Eq)]
66pub struct Module {
67 /// The module docstring.
68 pub doc: Doc,
69 /// `from __future__ import ...`, which must precede every other
70 /// statement, so it is a field rather than one more import.
71 pub future: Vec<Ident>,
72 /// `import` lines, in written order.
73 pub imports: Vec<Import>,
74 /// The body, in written order.
75 pub body: Vec<Statement>,
76}
77
78/// One `import` line.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum Import {
81 /// `import a.b [as c]`
82 Module {
83 /// What is imported.
84 path: Path,
85 /// The name it is bound to, when it is not the path's own.
86 alias: Option<Ident>,
87 },
88 /// `from a.b import c [as d], e`
89 From {
90 /// The module imported from.
91 path: Path,
92 /// What is taken out of it, in written order.
93 names: Vec<ImportName>,
94 },
95}
96
97/// One name in a `from ... import ...`.
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct ImportName {
100 /// The name in the source module.
101 pub name: Ident,
102 /// The name it is bound to, when it differs.
103 pub alias: Option<Ident>,
104}
105
106/// One statement of a module or a class body.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub enum Statement {
109 /// A class definition.
110 Class(Class),
111 /// `name: Type` or `name: Type = value` — an annotated binding.
112 ///
113 /// One node for a dataclass field, a `Final` constant and a bitset's
114 /// bitfield: Python spells all three the same way, and a tree that
115 /// distinguished them would be recording the rule's intent rather
116 /// than the syntax.
117 Annotated(Annotated),
118 /// `name = value` — a binding with no annotation.
119 ///
120 /// What a type alias and an enumeration member take: `RED = 0` inside
121 /// an `enum.IntEnum` is a member and `RED: int = 0` is not, so the
122 /// two forms are not interchangeable.
123 Assign(Assign),
124 /// A function definition.
125 Function(Function),
126 /// `place = value` — a store into somewhere that already exists.
127 ///
128 /// `v.field = x`, `v[i] = x`, `zd_wire = 3`. Distinct from
129 /// [`Statement::Assign`] for the reason the module documentation
130 /// gives: a binding introduces a name and a store does not, and only
131 /// the first of the two may appear where an enumeration member is
132 /// read off the class body.
133 Set {
134 /// Where the value goes.
135 place: Expr,
136 /// The value.
137 value: Expr,
138 },
139 /// An expression evaluated for its effect.
140 ///
141 /// What a call to the runtime is: `w.write_u32(v.x)`. Python has no
142 /// `(void)` cast and no discarded-result diagnostic, so a value the
143 /// generated code reads and does not keep is written as the call and
144 /// nothing else.
145 Expr(Expr),
146 /// `if condition: ... [else: ...]`
147 ///
148 /// No `elif`. A chain is an `else` whose body is one more
149 /// [`Statement::If`], which is what `elif` abbreviates and what the
150 /// codec walk builds; a separate arm would be a second spelling of
151 /// one tree.
152 If {
153 /// The condition.
154 condition: Expr,
155 /// The body taken when it holds; never empty.
156 then: Vec<Statement>,
157 /// The body taken when it does not.
158 otherwise: Option<Vec<Statement>>,
159 },
160 /// `for counter in range(bound): ...`
161 ///
162 /// A counted loop and not the general `for x in y`. The codec walk's
163 /// `CodecTarget::count_up` asks for a count from zero and nothing
164 /// else; a tree that could hold any iterable would be recording a
165 /// loop this column never writes.
166 For {
167 /// The induction variable.
168 counter: Ident,
169 /// The exclusive bound.
170 bound: Expr,
171 /// The body; never empty.
172 body: Vec<Statement>,
173 },
174 /// `while condition: ...`
175 While {
176 /// The condition.
177 condition: Expr,
178 /// The body; never empty.
179 body: Vec<Statement>,
180 },
181 /// `break`.
182 Break,
183 /// `return`, with or without a value.
184 Return(Option<Expr>),
185 /// A `#` comment on lines of its own.
186 Comment(Doc),
187 /// `pass`.
188 ///
189 /// A body has to hold something. A rule that produced an empty class
190 /// emits this rather than leaving the renderer to notice, because a
191 /// renderer that inserted it would be deciding what the tree says.
192 Pass,
193 /// `@verbatim` text, copied into the output unchanged — OMG IDL 4.2
194 /// §8.3.5.1 asks for the text "verbatim", so raw lines and never a
195 /// comment. At the indentation of the suite it stands in.
196 Verbatim(String),
197}
198
199/// `def name(params) -> returns: ...`
200#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct Function {
202 /// The docstring, written as the first statement of the body.
203 pub doc: Doc,
204 /// Decorators, in written order, outermost first.
205 pub decorators: Vec<Decorator>,
206 /// The name.
207 pub name: Ident,
208 /// The parameters, in written order.
209 pub params: Vec<Param>,
210 /// The return annotation. `None` leaves it unwritten, which is not
211 /// the same as `-> None`: the first says nothing and the second says
212 /// the function answers with nothing.
213 pub returns: Option<Type>,
214 /// The body, in written order; never empty.
215 pub body: Vec<Statement>,
216}
217
218/// One parameter of a [`Function`].
219///
220/// No `*args`, no `**kwargs` and no positional-only marker: a generated
221/// codec takes a fixed list and a field nothing sets would be a renderer
222/// arm nothing reviews.
223#[derive(Debug, Clone, PartialEq, Eq)]
224pub struct Param {
225 /// The name.
226 pub name: Ident,
227 /// The annotation, absent where the parameter carries none.
228 pub type_: Option<Type>,
229 /// The default value, absent where it has none.
230 pub default: Option<Expr>,
231}
232
233/// `name: Type` or `name: Type = value`.
234#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct Annotated {
236 /// Comment lines written above it.
237 pub doc: Doc,
238 /// The name bound.
239 pub name: Ident,
240 /// The annotation.
241 pub type_: Type,
242 /// The value, absent for a declaration with no initialiser.
243 pub value: Option<Expr>,
244}
245
246/// `name = value`.
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct Assign {
249 /// Comment lines written above it.
250 pub doc: Doc,
251 /// The name bound.
252 pub name: Ident,
253 /// The value.
254 pub value: Expr,
255}
256
257/// `class Name(bases): ...`
258#[derive(Debug, Clone, PartialEq, Eq)]
259pub struct Class {
260 /// The docstring, written as the first statement of the body.
261 pub doc: Doc,
262 /// Decorators, in written order, outermost first.
263 pub decorators: Vec<Decorator>,
264 /// The name.
265 pub name: Ident,
266 /// Base classes, in written order.
267 ///
268 /// No class keywords beside them: `metaclass=` and `boundary=` have
269 /// no part in a type declaration, and a field nothing sets would be
270 /// a renderer arm nothing reviews.
271 pub bases: Vec<Type>,
272 /// The body, in written order.
273 pub body: Vec<Statement>,
274}
275
276/// One decorator: `@path(args)`.
277#[derive(Debug, Clone, PartialEq, Eq)]
278pub struct Decorator {
279 /// The name applied.
280 pub path: Path,
281 /// Its arguments; empty means the decorator is written bare, with no
282 /// parentheses at all. `@dataclasses.dataclass` and
283 /// `@dataclasses.dataclass()` are the same thing, and the bare form
284 /// is the conventional one.
285 pub args: Vec<Arg>,
286}
287
288/// One argument of a call or a decorator.
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub enum Arg {
291 /// A positional argument.
292 Positional(Expr),
293 /// `name=value`
294 Keyword(Keyword),
295}
296
297/// `name=value`, in a call, a decorator or a class header.
298#[derive(Debug, Clone, PartialEq, Eq)]
299pub struct Keyword {
300 /// The parameter's name.
301 pub name: Ident,
302 /// The value.
303 pub value: Expr,
304}
305
306/// A type annotation.
307///
308/// Python writes an annotation as an expression, so every form here is
309/// also an [`Expr`] as far as the grammar is concerned. They are separate
310/// types anyway: a rule that could put an arbitrary expression where an
311/// annotation belongs could put `list[1]` there, and nothing between the
312/// rule and the file would notice.
313#[derive(Debug, Clone, PartialEq, Eq)]
314pub enum Type {
315 /// A name or a dotted name: `int`, `a.b.Point`.
316 Name(Path),
317 /// `base[arg, ...]` — `list[int]`, `tuple[int, int]`.
318 Subscript {
319 /// What is subscripted.
320 base: Path,
321 /// The arguments, in written order.
322 args: Vec<Type>,
323 },
324 /// `typing.Optional[T]`.
325 ///
326 /// Its own form rather than a [`Type::Subscript`]: `Optional` takes
327 /// exactly one argument, and the tree is where that is true.
328 Optional(Box<Type>),
329 /// `typing.Annotated[T, m, ...]` — the type, and what it means.
330 ///
331 /// The one place an [`Expr`] appears inside a [`Type`], because that
332 /// is what `typing.Annotated` is: a type paired with values a static
333 /// checker passes through and a reader at run time can read back with
334 /// `typing.get_type_hints(..., include_extras=True)`.
335 Annotated {
336 /// The type a checker sees.
337 base: Box<Type>,
338 /// The metadata, in written order; never empty.
339 markers: Vec<Expr>,
340 },
341}
342
343impl Type {
344 /// A type spelled by one name.
345 #[must_use]
346 pub fn name(text: &str) -> Self {
347 Self::Name(Path::single(text))
348 }
349
350 /// `base[args]`.
351 #[must_use]
352 pub fn subscript(base: Path, args: Vec<Self>) -> Self {
353 Self::Subscript { base, args }
354 }
355}
356
357/// An expression.
358#[derive(Debug, Clone, PartialEq, Eq)]
359pub enum Expr {
360 /// A literal.
361 Literal(Literal),
362 /// A name or a dotted name.
363 Path(Path),
364 /// `func(args)`.
365 Call {
366 /// What is called.
367 func: Path,
368 /// The arguments, in written order.
369 args: Vec<Arg>,
370 },
371 /// `(a, b)`.
372 Tuple(Vec<Expr>),
373 /// `[a, b]`.
374 ///
375 /// A list and not a tuple, which is a different value: a tuple
376 /// cannot be written into, and the reader's collection slots are
377 /// exactly what has to be.
378 List(Vec<Expr>),
379 /// `{k: v}`.
380 Dict(Vec<(Expr, Expr)>),
381 /// `value.name` — an attribute of something that is not a bare name.
382 ///
383 /// [`Expr::Path`] already spells `a.b.C`, and that is a dotted
384 /// *name*: every segment is an identifier. This is the other thing —
385 /// an attribute of a computed value, `v[i].key`, `zd_h.member_id` —
386 /// which a path cannot hold because its object is not a name.
387 Attribute {
388 /// What the attribute is taken of.
389 value: Box<Expr>,
390 /// The attribute.
391 name: Ident,
392 },
393 /// `value[index]`.
394 Index {
395 /// What is subscripted.
396 value: Box<Expr>,
397 /// The subscript.
398 index: Box<Expr>,
399 },
400 /// `start:end` — a slice, which is a subscript and nothing else.
401 ///
402 /// Only ever the `index` of an [`Expr::Index`]; Python has no other
403 /// position a slice is an expression in. Both ends are optional and
404 /// an absent one is written as nothing, which is what `v[:n]` is.
405 /// No step: §7.3.1.2.1.12.3's TRIM keeps a prefix, and a stride
406 /// would be a form with no producer.
407 Slice {
408 /// Where the slice starts, absent for the beginning.
409 start: Option<Box<Expr>>,
410 /// Where it ends, absent for the end.
411 end: Option<Box<Expr>>,
412 },
413 /// `op value`, written as given.
414 ///
415 /// `not x` and `~x`. The operator carries its own trailing space
416 /// where Python needs one, because `notx` is an identifier and
417 /// `~x` takes none — a renderer that inserted a space would be
418 /// deciding which of the two this is.
419 Unary {
420 /// The operator, as written, including any trailing space.
421 op: &'static str,
422 /// The operand.
423 value: Box<Expr>,
424 },
425 /// Parentheses written on purpose.
426 ///
427 /// [`Expr::Binary`] brackets itself, so this is for the forms that
428 /// do not: a [`Expr::Unary`] whose operand binds more loosely than
429 /// the operator, and anywhere the walk asks for a bracket outright.
430 Paren(Box<Expr>),
431 /// `lhs op rhs`.
432 Binary {
433 /// The operator, as written.
434 op: &'static str,
435 /// The left operand.
436 lhs: Box<Expr>,
437 /// The right operand.
438 rhs: Box<Expr>,
439 },
440 /// A type written where a value belongs.
441 ///
442 /// Which is exactly what a Python type alias is: `X = list[int]` is
443 /// an assignment whose right-hand side is an annotation. Without
444 /// this the two sides would need two spellings of the same syntax,
445 /// and they could drift.
446 Annotation(Box<Type>),
447}
448
449impl Expr {
450 /// An expression that is one name.
451 #[must_use]
452 pub fn name(text: &str) -> Self {
453 Self::Path(Path::single(text))
454 }
455
456 /// An integer literal.
457 #[must_use]
458 pub fn int(value: i128) -> Self {
459 Self::Literal(Literal::int(value))
460 }
461}
462
463/// The name an IDL identifier takes in Python.
464///
465/// A trailing underscore where the identifier would otherwise bind over
466/// something the generated file itself relies on. PEP 8 gives that rule
467/// for the keyword case — `class_`, `lambda_` — and the same escape
468/// covers the other two cases, because in Python they are the same
469/// problem:
470///
471/// - a **keyword**, which does not parse as a name at all;
472/// - a **soft keyword** (`match`, `case`, `type`), which parses but reads
473/// as a statement to anyone scanning the file;
474/// - a **name the generated module binds** — `dataclasses`, `typing`,
475/// `int`, `list` — which would be shadowed. This is not hypothetical
476/// and it is not caught by the parser: an `@optional` member called
477/// `int` becomes a class attribute set to `None`, and
478/// `typing.get_type_hints` resolves annotations against the class
479/// namespace first, so every `int` in that class would then resolve to
480/// `None`;
481/// - **`mro`**, which `enum.EnumType` refuses as a member name. It is
482/// the only such name — `name`, `value`, `property` and `auto` are all
483/// accepted, measured against CPython 3.14 — and an enumeration
484/// carrying it raises `ValueError` when the module is imported.
485///
486/// The IDL name is not lost when this fires: the materialiser writes it
487/// into the field's `dataclasses.field(metadata=...)` and into a comment
488/// above every other kind of declaration.
489///
490/// A leading underscore is left alone. It is a legal Python identifier
491/// and renaming it would make the generated name differ from the IDL for
492/// no reason.
493#[must_use]
494pub fn escape_name(name: &str) -> String {
495 if RESERVED.contains(&name) {
496 let mut out = name.to_string();
497 out.push('_');
498 return out;
499 }
500 name.to_string()
501}
502
503/// Every name an escape has to move out of the way of.
504///
505/// The hard keywords and the soft ones are the language's (Python 3.13
506/// reference §2.3.1); the rest are the names a generated module binds or
507/// writes, which is why they are here and not only in a materialiser: a
508/// second backend spelling this set differently would produce two files
509/// that disagree about which names are safe.
510pub const RESERVED: &[&str] = &[
511 // Keywords.
512 "False",
513 "None",
514 "True",
515 "and",
516 "as",
517 "assert",
518 "async",
519 "await",
520 "break",
521 "class",
522 "continue",
523 "def",
524 "del",
525 "elif",
526 "else",
527 "except",
528 "finally",
529 "for",
530 "from",
531 "global",
532 "if",
533 "import",
534 "in",
535 "is",
536 "lambda",
537 "nonlocal",
538 "not",
539 "or",
540 "pass",
541 "raise",
542 "return",
543 "try",
544 "while",
545 "with",
546 "yield",
547 // Soft keywords: legal as names, and escaped anyway.
548 "case",
549 "match",
550 "type",
551 // The modules a generated file imports, and the name
552 // `from __future__ import annotations` binds.
553 "annotations",
554 "dataclasses",
555 "enum",
556 "typing",
557 // Refused by `enum.EnumType` as a member name, so an enumerator of
558 // this name would make the whole module fail to import.
559 "mro",
560 // The builtins a generated annotation is written in terms of.
561 "bool",
562 "bytes",
563 "float",
564 "int",
565 "list",
566 "object",
567 "str",
568 "tuple",
569];
570
571#[cfg(test)]
572mod tests {
573 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
574 use super::*;
575
576 #[test]
577 fn a_keyword_escapes_with_a_trailing_underscore() {
578 assert_eq!(escape_name("class"), "class_");
579 assert_eq!(escape_name("lambda"), "lambda_");
580 assert_eq!(escape_name("payload"), "payload");
581 }
582
583 #[test]
584 fn a_soft_keyword_escapes_although_it_would_parse() {
585 // Legal as an identifier, escaped anyway: a member called
586 // `match` reads as a statement to anyone scanning the file.
587 for name in ["match", "case", "type"] {
588 assert_eq!(escape_name(name), alloc::format!("{name}_"));
589 }
590 }
591
592 #[test]
593 fn a_name_the_generated_module_binds_escapes_too() {
594 // The case the parser does not catch. An `@optional` member
595 // called `int` becomes a class attribute set to `None`, and
596 // `typing.get_type_hints` resolves the class namespace before
597 // the module's — so every `int` in that class would resolve to
598 // `None` rather than to the builtin.
599 for name in ["int", "list", "tuple", "str", "typing", "dataclasses"] {
600 assert_eq!(escape_name(name), alloc::format!("{name}_"));
601 }
602 }
603
604 #[test]
605 fn the_one_name_an_enumeration_may_not_use_escapes() {
606 // `enum.EnumType` refuses it, so an enumerator called `mro`
607 // would make the whole generated module fail to import. It is
608 // the only one: `name`, `value`, `property` and `auto` are all
609 // accepted as members, measured against CPython 3.14.
610 assert_eq!(escape_name("mro"), "mro_");
611 assert_eq!(escape_name("name"), "name");
612 assert_eq!(escape_name("value"), "value");
613 }
614
615 #[test]
616 fn a_leading_underscore_is_left_alone() {
617 // Legal Python, and renaming it would make the generated name
618 // differ from the IDL for no reason.
619 assert_eq!(escape_name("_reserved"), "_reserved");
620 assert_eq!(escape_name("_d"), "_d");
621 }
622
623 #[test]
624 fn the_reserved_list_has_no_duplicates() {
625 // A duplicate would be harmless and would also mean the list was
626 // assembled by hand twice.
627 let mut seen: Vec<&str> = Vec::new();
628 for name in RESERVED {
629 assert!(!seen.contains(name), "{name} is listed twice");
630 seen.push(name);
631 }
632 }
633
634 #[test]
635 fn an_annotated_type_carries_its_metadata_beside_the_type_a_checker_sees() {
636 let type_ = Type::Annotated {
637 base: Box::new(Type::name("int")),
638 markers: alloc::vec![Expr::Call {
639 func: Path::single("IdlInt"),
640 args: alloc::vec![Arg::Keyword(Keyword {
641 name: Ident::new("bits"),
642 value: Expr::int(32),
643 })],
644 }],
645 };
646 let Type::Annotated { base, markers } = &type_ else {
647 panic!("not annotated");
648 };
649 assert_eq!(**base, Type::name("int"));
650 assert_eq!(markers.len(), 1);
651 }
652}