Skip to main content

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}