zerodds_lang_rules/adapter/python.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Python CST as the other end of a rule. Spec §3.2.
4//!
5//! The counterpart of [`crate::adapter::java`], [`crate::adapter::cpp`]
6//! and [`crate::adapter::c`]. Reading two of them side by side is the
7//! point: where they differ, the difference is the language's, and it is
8//! written down here rather than spread through a renderer.
9//!
10//! Values are resolved from the model, never from the overlay — the same
11//! rule the sibling materialisers follow and for the same reason
12//! (spec §2.3).
13//!
14//! # Python is the hardest of the five, and this is why
15//!
16//! The other four target languages state a width, a length and a
17//! signedness in the declaration itself. Python states none of them. A
18//! `long` is 32 bits and a Python `int` is arbitrary precision; an
19//! IDL `octet` and an IDL `unsigned long long` are the same Python type;
20//! `long a[3]` and `sequence<long> a` are both "a thing you can index".
21//! A backend that wrote `x: int` and stopped would produce a file that
22//! type-checks and a value the wire cannot hold, with nothing between
23//! the two to notice.
24//!
25//! So this column is arranged around one question: **for each IDL fact
26//! Python cannot enforce, where is it written down?** There are exactly
27//! two answers used here and no third.
28//!
29//! 1. **The Python type**, where Python can enforce it. Mutability
30//! (`list` against `tuple`), arity (`tuple[T, T, T]`), absence
31//! (`typing.Optional[T]`), and which class a member holds. A static
32//! checker rejects a program that gets these wrong.
33//! 2. **`typing.Annotated` metadata**, where it cannot. The width, the
34//! signedness, the bound, the digit count, the character width. A
35//! checker passes these through untouched; they are read back at run
36//! time with `typing.get_type_hints(cls, include_extras=True)`, which
37//! is what the codec pass will do.
38//!
39//! Nothing is left implicit. `x: int` never appears in generated output.
40//!
41//! ## The integer mapping
42//!
43//! | IDL | Python | metadata |
44//! |---|---|---|
45//! | `boolean` | `bool` | — |
46//! | `char` | `str` | `_IdlText(bits=8, bound=1)` |
47//! | `wchar` | `str` | `_IdlText(bits=16, bound=1)` |
48//! | `int8` | `int` | `_IdlInt(bits=8, signed=True)` |
49//! | `octet`, `uint8` | `int` | `_IdlInt(bits=8, signed=False)` |
50//! | `short` | `int` | `_IdlInt(bits=16, signed=True)` |
51//! | `unsigned short` | `int` | `_IdlInt(bits=16, signed=False)` |
52//! | `long` | `int` | `_IdlInt(bits=32, signed=True)` |
53//! | `unsigned long` | `int` | `_IdlInt(bits=32, signed=False)` |
54//! | `long long` | `int` | `_IdlInt(bits=64, signed=True)` |
55//! | `unsigned long long` | `int` | `_IdlInt(bits=64, signed=False)` |
56//! | `float` | `float` | `_IdlFloat(bits=32)` |
57//! | `double` | `float` | `_IdlFloat(bits=64)` |
58//! | `long double` | **`str`** | `_IdlFloat(bits=128)` |
59//!
60//! Each of those is written once as a module-level alias — `_Int32`,
61//! `_UInt64` — so a field reads `x: _Int32` and the width is one hop
62//! away rather than inline on every line.
63//!
64//! An alias exists exactly where the type's whole description is fixed.
65//! A `string<7>` and a `string<9>` are not the same type, so the string
66//! family is written inline instead — including the unbounded one, so
67//! that `string` and `string<7>` have one spelling between them rather
68//! than a name for the common case and a form for the rest.
69//!
70//! **Where this departs from every other column.** Java has to decide
71//! which of its signed types is wide enough to *hold* an unsigned IDL
72//! value, and `unsigned long long` ends up a `long` carrying a bit
73//! pattern rather than a number. Python has no such problem and so
74//! makes no such trade: every one of the ten integers is a Python `int`
75//! holding the number, and only the metadata differs. That is the one
76//! place where Python's lack of widths is an advantage, and it is worth
77//! saying because everywhere else it is the cost.
78//!
79//! ### `long double` is a `str` and never a `float`
80//!
81//! A Python `float` is a C `double` — 53 bits of significand. An IDL
82//! `long double` is a binary128 with 113. Writing one into a `float`
83//! loses sixty bits at the assignment, before any codec runs, with
84//! nothing to say it happened. That is the silent-substitution class
85//! this project is arranged against, and it is the same defect the
86//! three-column gate found in the C column when `long double` reached
87//! `double` there.
88//!
89//! So the member holds the **decimal text**, exactly as `fixed<P,S>`
90//! does and for the same reason: it is the one Python type that holds
91//! the value the source wrote without rounding it. The Java column
92//! reaches for `org.zerodds.wire.Float128` instead, which this column
93//! cannot: a generated Python module here imports nothing but
94//! `dataclasses`, `enum` and `typing`, so a runtime class would make
95//! every generated file depend on a package that is not part of this
96//! layer.
97//!
98//! ## The shapes
99//!
100//! | IDL | Python |
101//! |---|---|
102//! | `string<N>`, `wstring<N>` | `str`, with the width and bound in the metadata |
103//! | `fixed<P,S>` | `str`, holding the decimal |
104//! | `sequence<T>` | `list[T]` |
105//! | `sequence<T,N>` | `list[T]`, with the bound in the metadata |
106//! | `T[N]` | `tuple[T, …]` — **N** arguments, not an ellipsis |
107//! | `T[N][M]` | `tuple[tuple[T, …M], …N]`, outer dimension outermost |
108//! | `map<K,V>` | `list[tuple[K, V]]` |
109//! | `union` | a dataclass with `_d` and one optional field per case |
110//! | `enum` | `enum.IntEnum`; a member of it is an `int` |
111//! | `bitmask` | `enum.IntFlag`; a member of it is an `int` |
112//! | `bitset` | a dataclass of one field per bitfield |
113//! | `@optional T m` | `typing.Optional[T]`, defaulting to `None` |
114//! | `typedef` | nothing; a comment, and the target's type at each use |
115//! | `module` | a class the declarations nest in |
116//!
117//! ### `sequence<T>` is a `list` and `T[N]` is a `tuple`
118//!
119//! Not a style choice. An IDL array has a length that is part of its
120//! type and a sequence does not, and those are the two Python built-ins
121//! that say exactly that: a `list` can be appended to and a `tuple`
122//! cannot. The array's length goes further — `tuple[int, int, int]`
123//! names three elements, and a static checker rejects a two-element
124//! literal — which makes the length the **one** IDL fact in this whole
125//! column that Python's own type system enforces. Writing
126//! `tuple[int, ...]` and putting the length in the metadata would give
127//! that up for nothing.
128//!
129//! The cost is stated rather than hidden: `long a[1000]` produces an
130//! annotation with a thousand arguments in it. Nothing truncates it and
131//! there is no threshold at which the spelling changes, because a
132//! threshold would mean the same IDL construct had two Python forms
133//! depending on a number nobody chose.
134//!
135//! ### `map<K,V>` is a `list` of pairs and not a `dict`
136//!
137//! Two independent reasons, either of which decides it.
138//!
139//! A `dict` requires hashable keys. A generated struct is a
140//! `@dataclasses.dataclass` with `eq=True`, which sets `__hash__` to
141//! `None` — so `dict[Point, int]` is a type that cannot hold anything,
142//! and `map<Point, long>` is ordinary IDL.
143//!
144//! And the entry order is a policy axis. `MapEntryOrder` exists to
145//! measure which order a peer puts the entries in, and a container that
146//! decided an order of its own would make the axis measure the container.
147//! A `list` of pairs keeps the sample's order where the sample put it.
148//! The Java column reaches the same answer through an array of a
149//! generated entry class; Python needs no generated class because
150//! `tuple[K, V]` already is one.
151//!
152//! ### `@optional` is `typing.Optional[T]` and `None` is the absence
153//!
154//! One field, not a field beside a flag. XTypes 1.3 §7.4.1.2.3 makes
155//! **absence** the value, not a zero, and `None` is Python's. A
156//! `bool _has_m` beside an `m` would be two things that can disagree,
157//! which is what the C column has to carry because C has no `None`.
158//!
159//! A non-optional member is emitted with no default at all, so
160//! constructing an aggregate without it is an error rather than a
161//! silently zero-filled sample.
162//!
163//! ### An enumeration is an `enum.IntEnum` and a member of it is an `int`
164//!
165//! Both halves matter.
166//!
167//! The **declaration** is a real `enum.IntEnum` because a Python enum
168//! member *is* an `int` — `Colour.RED == 0` is true and
169//! `Colour.RED + 1` is an `int`. Java cannot do this, because a Java
170//! `enum` constant's identity is its ordinal and `@value(-1)` is not an
171//! ordinal, which is why the Java column emits a class of `int`
172//! constants and loses the type.
173//!
174//! The **member** is nonetheless an `int` and not the enumeration:
175//! `Colour(7)` raises `ValueError`, and an `@appendable` writer that
176//! added an enumerator sends exactly that. A field typed `Colour` could
177//! only refuse the sample. So the member is
178//! `typing.Annotated[int, _IdlEnum(declared=Colour, bits=32)]` — the
179//! runtime type is what the wire can deliver, and which enumeration it
180//! is stays readable, in the metadata, where a codec can get at it.
181//!
182//! A bitmask is the same arrangement with `enum.IntFlag`.
183//!
184//! ### A bitset states the bit width and not a holder
185//!
186//! `bitfield<3> low` is three bits. C and Java have to round that up to
187//! the narrowest integer that holds it, because they have no three-bit
188//! type; Python has no fixed-width type at all, so nothing is rounded
189//! and the metadata states three. The declared holder of
190//! `bitfield<1, boolean>` names a range and not a wire form, and the
191//! width is what the packing uses — the same reading the Java column
192//! gives.
193//!
194//! ### A `typedef` declares nothing
195//!
196//! Python has type aliases, and this column does not use one, for a
197//! reason that is Python's own: an assignment is evaluated **eagerly**.
198//! `Position = a.Point` at module level runs when the module is
199//! imported, so it has to come after `class a`, and a typedef in one
200//! IDL module naming a type in another has no position in the nesting
201//! at which that is true. The alias would be a `NameError` at import,
202//! for exactly the cross-module cases and no others. So a typedef is
203//! transparent — the use takes the target's type — and a comment
204//! carries the name and what it resolved to, which is what the Java
205//! column does and for a different reason.
206//!
207//! ### Modules are nested classes
208//!
209//! Python's own unit of scope is the module, and one IDL file becomes
210//! one Python module, so the file's top level is already the global IDL
211//! scope; nothing needs an outer class the way the Java column does. A
212//! nested IDL module becomes a class, which is the only Python
213//! construct that nests, carries a name and can be referred to through
214//! a dotted path.
215//!
216//! Every reference to a generated type is written from the module root
217//! down — `a.b.Point`. Unlike Java there is no capture hazard in doing
218//! so: `typing.get_type_hints` resolves an annotation against the
219//! module globals, where `a` is bound, and a *member* named `a` is not
220//! in those globals. The escape below covers the case that does bite.
221//!
222//! ### Identifiers
223//!
224//! Through [`zerodds_lang_cst::python::escape_name`], which appends an
225//! underscore — PEP 8's own rule for this case. It moves three kinds of
226//! name: a keyword, a soft keyword, and a name the generated module
227//! itself binds (`int`, `list`, `typing`). The third is the one that is
228//! invisible without it: an `@optional` member named `int` becomes a
229//! class attribute set to `None`, and `typing.get_type_hints` resolves
230//! against the class namespace before the module's, so every `int` in
231//! that class would then resolve to `None`.
232//!
233//! **The IDL name is not lost.** A renamed field carries it in
234//! `dataclasses.field(metadata={"idl": …})`, which is machine-readable;
235//! every other renamed declaration carries it in a comment on the line
236//! above. And two IDL names that would escape onto one Python name in
237//! one scope are refused rather than letting the second take the first's
238//! declaration.
239//!
240//! ## What is refused
241//!
242//! `any`, `ValueBase`, an object reference and an annotation type have
243//! no Python data form here and are named as
244//! [`MaterialiseError::NoLanguageForm`] rather than left out of the
245//! class. Python would happily take `object` for all four, which is
246//! exactly why it is not written: a member typed `object` is a field no
247//! codec can write, from a generator that reported success.
248
249use crate::adapter::c::ordered_types;
250use crate::adapter::constant;
251use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
252use crate::apply::Applied;
253use crate::verbatim;
254use seesaw_tgg::ident::GhostId;
255use zerodds_ast::{Ast, Bound, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
256use zerodds_ast_core::{Id, NodeId};
257use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
258use zerodds_lang_cst::python::{
259 Annotated, Arg, Assign, Class, Decorator, Expr, Import, Keyword, Module, Statement, Type,
260 escape_name,
261};
262
263/// How deep a module chain is followed before the model is taken to be
264/// malformed. IDL nests nowhere near this.
265const MAX_MODULE_DEPTH: usize = 64;
266
267/// How deep a type's Python spelling is followed.
268///
269/// A `sequence<sequence<…>>` nests once per level; a chain longer than
270/// this is a cycle the resolver did not catch, and recursing into it
271/// would end the process rather than report anything.
272const MAX_TYPE_DEPTH: usize = 64;
273
274/// The names the generated preamble binds.
275///
276/// Every one starts with an underscore, and an IDL identifier cannot:
277/// IDL 4.2 §7.2.3 makes the first character an ASCII letter, and the
278/// escaped form `_keyword` of §7.2.3.2 denotes the identifier *without*
279/// the underscore. So this list should never fire. It is checked anyway,
280/// because "should never" about a name collision is the assumption that
281/// makes one declaration silently take another's.
282const PRELUDE_NAMES: &[&str] = &[
283 "_IdlInt",
284 "_IdlFloat",
285 "_IdlText",
286 "_IdlFixed",
287 "_IdlSequence",
288 "_IdlEnum",
289 "_Int8",
290 "_UInt8",
291 "_Int16",
292 "_UInt16",
293 "_Int32",
294 "_UInt32",
295 "_Int64",
296 "_UInt64",
297 "_Float32",
298 "_Float64",
299 "_Float128",
300 "_Char",
301 "_WChar",
302];
303
304/// Build the Python module a rule application produced.
305///
306/// The whole file and not a list of declarations: which imports the
307/// preamble needs and which markers the annotations are written in terms
308/// of are part of what this column decides, and a caller assembling them
309/// separately would be a second place where they are decided.
310///
311/// # Errors
312/// [`MaterialiseError`] when a value the output has to state does not
313/// evaluate, a type has no Python shape, or two declarations would take
314/// one name in one scope — never a silently dropped declaration.
315pub fn python_module(ast: &Ast, applied: &Applied) -> Result<Module, MaterialiseError> {
316 let mut items: Vec<Pending> = Vec::new();
317
318 // The same walk the C and Java columns use. Python needs no
319 // declaration before use — `from __future__ import annotations`
320 // makes every annotation a string, evaluated only when something
321 // asks — so the order is not load-bearing for the interpreter. It is
322 // load-bearing for the output, which must not move between two runs
323 // over one model.
324 for id in ordered_types(ast) {
325 let start = items.len();
326 declare(ast, applied, id, &mut items)?;
327 let Ok(node) = ast.type_(id) else {
328 continue;
329 };
330 let module = node.module;
331 let name = escape_name(&node.name);
332 verbatim::weave_with(
333 verbatim::declaration_slots(ast, id, verbatim::aliases::PYTHON),
334 start,
335 &mut items,
336 |text| Pending {
337 module,
338 statement: Statement::Verbatim(text),
339 },
340 |declared, begin, end| {
341 for pending in declared {
342 if verbatim::Carrier::take_into_body(&mut pending.statement, &name, begin, end)
343 {
344 return;
345 }
346 }
347 },
348 );
349 }
350
351 // Constants and exceptions are not types, so they are walked
352 // separately — the model keeps them in arenas of their own, and the
353 // sibling materialisers do the same.
354 for (id, node) in ast.constants() {
355 if ghost_created_for(applied, node.node_id, "PythonConst").is_none() {
356 continue;
357 }
358 let _ = id;
359 // `eval_const` and not `eval_integral`: `const string MODULE_ID`
360 // evaluates perfectly well and is not a number.
361 let value = ast
362 .eval_const(node.expr)
363 .map_err(|_| MaterialiseError::Unevaluated {
364 construct: "constant's value".to_string(),
365 at: node.name.clone(),
366 })?;
367 let (type_, kind) = match node.type_.resolved() {
368 Some(target) => (
369 python_member_type(ast, target, &node.name)?,
370 scalar_kind(ast, target),
371 ),
372 None => (alias(INT32), Some(PrimitiveKind::Int32)),
373 };
374 let value = constant_expr(kind, &value, &node.name)?;
375 items.push(Pending {
376 module: node.module,
377 statement: Statement::Annotated(Annotated {
378 doc: renamed_note(&node.name),
379 name: Ident::new(&escape_name(&node.name)),
380 // `typing.Final`: a constant that something reassigned
381 // would be a constant only in the IDL.
382 type_: Type::Subscript {
383 base: Path::new(&["typing", "Final"]),
384 args: vec![type_],
385 },
386 value: Some(value),
387 }),
388 });
389 }
390
391 for (id, node) in ast.exceptions() {
392 if ghost_created_for(applied, node.node_id, "PythonError").is_none() {
393 continue;
394 }
395 let _ = id;
396 let mut body = Vec::new();
397 for member in &node.members {
398 let Ok(member_node) = ast.member(*member) else {
399 return Err(MaterialiseError::Unevaluated {
400 construct: "exception member the model listed".to_string(),
401 at: node.name.clone(),
402 });
403 };
404 let Some(target) = member_node.type_.resolved() else {
405 return Err(MaterialiseError::Unevaluated {
406 construct: "exception member type".to_string(),
407 at: member_node.name.clone(),
408 });
409 };
410 body.push(field(
411 &member_node.name,
412 python_member_type(ast, target, &member_node.name)?,
413 member_node.facets.is_optional(),
414 ));
415 }
416 items.push(Pending {
417 module: node.module,
418 statement: dataclass(
419 &node.name,
420 body,
421 Doc::lines(&[
422 "An exception, as the structure its members make.",
423 "",
424 "A dataclass and not a subclass of Exception: raising is a",
425 "run-time decision the RPC arc makes, and a BaseException",
426 "subclass carries __traceback__ and args, which are two more",
427 "attributes than the wire form has.",
428 ]),
429 ),
430 });
431 }
432
433 let mut root = Scope::default();
434 for item in items {
435 insert(ast, &mut root, item)?;
436 }
437 let body = scope_body(root, 0)?;
438 check_scope(&body, 0)?;
439
440 let mut statements = prelude();
441 statements.extend(body);
442 let statements = verbatim::weave_file(ast, verbatim::aliases::PYTHON, statements);
443
444 Ok(Module {
445 doc: Doc::lines(&[
446 "The Python shape of one IDL file.",
447 "",
448 "Generated by zerodds-lang-rules. Do not edit.",
449 "",
450 "Every IDL fact Python cannot enforce in a type is written into",
451 "typing.Annotated metadata: the width of an integer, the bound of a",
452 "string or a sequence, the digit count of a fixed. Read it back with",
453 "typing.get_type_hints(cls, include_extras=True).",
454 "",
455 "Requires Python 3.10 or newer, for dataclasses.dataclass(kw_only=True).",
456 ]),
457 // Every annotation becomes a string, so nothing here has to be
458 // declared before it is named and a nested module class may be
459 // referred to from anywhere in the file.
460 future: vec![Ident::new("annotations")],
461 imports: vec![
462 Import::Module {
463 path: Path::single("dataclasses"),
464 alias: None,
465 },
466 Import::Module {
467 path: Path::single("enum"),
468 alias: None,
469 },
470 Import::Module {
471 path: Path::single("typing"),
472 alias: None,
473 },
474 ],
475 body: statements,
476 })
477}
478
479/// One declaration, kept with the module that declared it.
480struct Pending {
481 /// The IDL module it was declared in.
482 module: Id<ModuleNode>,
483 /// What it materialised as.
484 statement: Statement,
485}
486
487/// `@verbatim` in the Python column: `BEGIN_DECLARATION` and
488/// `END_DECLARATION` open and close a class body — the dataclass of a
489/// structure or a union, the `IntEnum` of an enumeration; an alias takes
490/// them adjacent.
491impl verbatim::Carrier for Statement {
492 fn verbatim(text: String) -> Self {
493 Self::Verbatim(text)
494 }
495
496 fn take_into_body(
497 &mut self,
498 name: &str,
499 begin: &mut Vec<String>,
500 end: &mut Vec<String>,
501 ) -> bool {
502 let Self::Class(class) = self else {
503 return false;
504 };
505 if class.name.as_str() != name {
506 return false;
507 }
508 // A `pass` stays: the text may be nothing but comments, and a
509 // class whose suite holds no statement does not parse.
510 let mut body: Vec<Statement> = begin.drain(..).map(Statement::Verbatim).collect();
511 body.append(&mut class.body);
512 body.extend(end.drain(..).map(Statement::Verbatim));
513 class.body = body;
514 true
515 }
516}
517
518/// One nesting level of the output: a module's declarations and the
519/// modules declared inside it.
520///
521/// A tree and not a list of chains. Grouping by module id alone would
522/// emit `module common { struct Robot; }` and
523/// `module common { module util { … } }` as two classes called `common`
524/// in one scope, which is the `tests/interop-corpus/module-issue41-shape`
525/// arrangement.
526#[derive(Default)]
527struct Scope {
528 /// What this module declares directly, in declaration order.
529 body: Vec<Statement>,
530 /// The modules declared inside it, in first-seen order.
531 children: Vec<(Id<ModuleNode>, String, Scope)>,
532}
533
534/// Put one declaration in the scope its module names.
535///
536/// # Errors
537/// [`MaterialiseError::NoModule`] when the model does not answer for a
538/// module in the chain, or the chain runs past [`MAX_MODULE_DEPTH`].
539fn insert(ast: &Ast, root: &mut Scope, item: Pending) -> Result<(), MaterialiseError> {
540 let chain = module_chain(ast, item.module)?;
541 let mut scope = root;
542 for (id, name) in chain {
543 let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
544 Some(at) => at,
545 None => {
546 scope.children.push((id, name, Scope::default()));
547 scope.children.len() - 1
548 }
549 };
550 scope = &mut scope
551 .children
552 .get_mut(at)
553 .ok_or_else(|| MaterialiseError::NoModule {
554 at: module_handle(item.module),
555 })?
556 .2;
557 }
558 scope.body.push(item.statement);
559 Ok(())
560}
561
562/// A scope's statements, with each module inside it as a nested class.
563///
564/// # Errors
565/// [`MaterialiseError::NoModule`] when the nesting runs past
566/// [`MAX_MODULE_DEPTH`], which `insert` has already bounded.
567///
568/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
569fn scope_body(scope: Scope, depth: usize) -> Result<Vec<Statement>, MaterialiseError> {
570 if depth > MAX_MODULE_DEPTH {
571 return Err(MaterialiseError::NoModule {
572 at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
573 });
574 }
575 let mut out = scope.body;
576 for (_, name, child) in scope.children {
577 let body = scope_body(child, depth + 1)?;
578 out.push(Statement::Class(Class {
579 doc: Doc::line("An IDL module, as the class its declarations nest in."),
580 decorators: Vec::new(),
581 name: Ident::new(&name),
582 bases: Vec::new(),
583 // A class body has to hold something. An IDL module that
584 // declared nothing this column emits would otherwise render
585 // as a header with no block under it, which is a
586 // SyntaxError.
587 body: if body.is_empty() {
588 vec![Statement::Pass]
589 } else {
590 body
591 },
592 }));
593 }
594 Ok(out)
595}
596
597/// The modules a declaration nests in, outermost first, with the class
598/// name each takes.
599///
600/// The root module is the anonymous global scope and is the Python
601/// module itself, so it is not a class.
602///
603/// # Errors
604/// [`MaterialiseError::NoModule`] when the model does not answer for a
605/// module, or the parent chain runs past [`MAX_MODULE_DEPTH`]. Returning
606/// the declaration unnested instead would put it in another scope and say
607/// nothing, which is the same class of substitution as writing the wrong
608/// octet.
609fn module_chain(
610 ast: &Ast,
611 module: Id<ModuleNode>,
612) -> Result<Vec<(Id<ModuleNode>, String)>, MaterialiseError> {
613 let mut out = Vec::new();
614 let mut current = Some(module);
615 let mut depth = 0;
616 while let Some(id) = current {
617 if depth > MAX_MODULE_DEPTH {
618 return Err(MaterialiseError::NoModule {
619 at: format!(
620 "{} — the parent chain runs past {MAX_MODULE_DEPTH}",
621 module_handle(module)
622 ),
623 });
624 }
625 let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
626 at: module_handle(id),
627 })?;
628 // The root has no name and no parent, and produces no class.
629 if node.parent.is_none() {
630 break;
631 }
632 out.push((id, escape_name(&node.name)));
633 current = node.parent;
634 depth += 1;
635 }
636 out.reverse();
637 Ok(out)
638}
639
640/// The module handle, named without the node the model would not answer
641/// for. Slot and generation are what the arena was asked for.
642fn module_handle(module: Id<ModuleNode>) -> String {
643 format!("#{}@{}", module.slot(), module.generation())
644}
645
646/// Refuse two declarations that would take one name in one scope.
647///
648/// Python binds a name; it does not declare one. `class P: ...` twice in
649/// one body is not an error, it is a rebinding, and the second silently
650/// wins. Two IDL names cannot collide before escaping — IDL forbids it —
651/// but `class` and `class_` both escape onto `class_`, and so do a
652/// member named `int` and a member named `int_`.
653///
654/// zerodds-lint: recursion-depth 64 (module nesting; bounded by `MAX_MODULE_DEPTH`)
655fn check_scope(body: &[Statement], depth: usize) -> Result<(), MaterialiseError> {
656 if depth > MAX_MODULE_DEPTH {
657 return Err(MaterialiseError::NoModule {
658 at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
659 });
660 }
661 let mut seen: Vec<&str> = Vec::new();
662 for statement in body {
663 let name = match statement {
664 Statement::Class(class) => class.name.as_str(),
665 Statement::Annotated(binding) => binding.name.as_str(),
666 Statement::Assign(binding) => binding.name.as_str(),
667 Statement::Function(function) => function.name.as_str(),
668 // The forms the codec pass added to the tree. None of them
669 // binds a name a later declaration could replace — a store
670 // writes into a place that already exists, and the rest are
671 // control flow — and none of them reaches a *materialised*
672 // module at all: this walk sees only what
673 // [`python_module`] built. Named rather than swept up with
674 // a wildcard so that a form which does start binding names
675 // fails here instead of being scoped by omission.
676 Statement::Set { .. }
677 | Statement::Expr(_)
678 | Statement::If { .. }
679 | Statement::For { .. }
680 | Statement::While { .. }
681 | Statement::Break
682 | Statement::Return(_)
683 | Statement::Comment(_)
684 | Statement::Pass
685 | Statement::Verbatim(_) => continue,
686 };
687 if seen.contains(&name) {
688 return Err(MaterialiseError::NoLanguageForm {
689 construct: format!(
690 "a second declaration binding the name `{name}` in one scope, \
691 which would silently replace the first"
692 ),
693 language: "Python",
694 at: name.to_string(),
695 });
696 }
697 if PRELUDE_NAMES.contains(&name) {
698 return Err(MaterialiseError::NoLanguageForm {
699 construct: format!(
700 "a declaration named `{name}`, which is one of the names the \
701 generated preamble binds"
702 ),
703 language: "Python",
704 at: name.to_string(),
705 });
706 }
707 seen.push(name);
708 if let Statement::Class(class) = statement {
709 check_scope(&class.body, depth + 1)?;
710 }
711 }
712 Ok(())
713}
714
715/// One type's Python declarations, appended where they belong.
716#[allow(clippy::too_many_lines)]
717fn declare(
718 ast: &Ast,
719 applied: &Applied,
720 id: Id<TypeNode>,
721 items: &mut Vec<Pending>,
722) -> Result<(), MaterialiseError> {
723 let Ok(node) = ast.type_(id) else {
724 return Ok(());
725 };
726 match &node.kind {
727 // A primitive, a string, a `fixed`, a sequence and a map are all
728 // spellings Python already has — `list[tuple[K, V]]` needs no
729 // entry class, which is the one place this column declares less
730 // than the Java one. None of them declares anything.
731 TypeKind::Primitive(_)
732 | TypeKind::String { .. }
733 | TypeKind::Fixed { .. }
734 | TypeKind::Sequence { .. }
735 | TypeKind::Map { .. } => {}
736
737 // Neither does an array — but every dimension is evaluated
738 // anyway, and here it has to be: the length is written into the
739 // annotation as that many tuple arguments, so a dimension that
740 // does not evaluate has no spelling at all. The C and Java
741 // columns fail on the same input, and the columns' failure sets
742 // stay comparable.
743 TypeKind::Array { dims, .. } => {
744 for dim in dims {
745 ast.eval_integral(*dim)
746 .map_err(|_| MaterialiseError::Unevaluated {
747 construct: "array length".to_string(),
748 at: named(&node.name, node.node_id),
749 })?;
750 }
751 }
752
753 // A typedef has no Python declaration; see the module
754 // documentation for why an alias would be a `NameError` at
755 // import. The comment is what keeps it from being a silent
756 // omission: the name, and what it resolved to.
757 TypeKind::Alias { aliased } => {
758 if node.name.is_empty() {
759 return Ok(());
760 }
761 if ghost_created_for(applied, node.node_id, "PythonAlias").is_none() {
762 return Ok(());
763 }
764 let Some(target) = aliased.resolved() else {
765 return Err(MaterialiseError::Unevaluated {
766 construct: "typedef target".to_string(),
767 at: node.name.clone(),
768 });
769 };
770 let spelling =
771 zerodds_lang_render::python::type_(&python_member_type(ast, target, &node.name)?);
772 items.push(Pending {
773 module: node.module,
774 statement: Statement::Comment(Doc::line(&format!(
775 "typedef {spelling} {}; -- not a Python alias, which is evaluated \
776 eagerly and could not be ordered against the classes it names, so \
777 every use takes the spelling above.",
778 node.name
779 ))),
780 });
781 }
782
783 TypeKind::Enumeration { enumerators } => {
784 if ghost_created_for(applied, node.node_id, "PythonEnum").is_none() {
785 return Ok(());
786 }
787 let mut body = Vec::new();
788 for enumerator in enumerators {
789 let Ok(enum_node) = ast.enumerator(*enumerator) else {
790 return Err(MaterialiseError::Unevaluated {
791 construct: "enumerator the model listed".to_string(),
792 at: node.name.clone(),
793 });
794 };
795 if ghost_created_for(applied, enum_node.node_id, "PythonEnumerator").is_none() {
796 continue;
797 }
798 let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
799 MaterialiseError::Unevaluated {
800 construct: "enumerator value".to_string(),
801 at: enum_node.name.clone(),
802 }
803 })?;
804 // Refused outside the 32 bits XTypes 1.3 §7.4.3.5.3
805 // gives an enumerated literal, and written as the number
806 // the source wrote otherwise. The Java column has to
807 // fold a value above `int`'s range onto the pattern it
808 // occupies; a Python `int` holds it, so nothing folds.
809 let value = in_range(
810 value,
811 i128::from(i32::MIN),
812 i128::from(u32::MAX),
813 "32-bit enumerated literal",
814 &enum_node.name,
815 )?;
816 body.push(Statement::Assign(Assign {
817 doc: renamed_note(&enum_node.name),
818 name: Ident::new(&escape_name(&enum_node.name)),
819 value: Expr::int(value),
820 }));
821 }
822 items.push(Pending {
823 module: node.module,
824 statement: enum_class(
825 &node.name,
826 "IntEnum",
827 body,
828 Doc::lines(&[
829 "An enumeration. An enum.IntEnum member is an int, so the names",
830 "and the values are one declaration rather than two.",
831 "",
832 "A member of this type is nonetheless annotated int and not this",
833 "class: an @appendable writer may send a value the reader's IDL",
834 "does not list, and Colour(7) raises ValueError.",
835 ]),
836 ),
837 });
838 }
839
840 TypeKind::Bitmask { bits } => {
841 if ghost_created_for(applied, node.node_id, "PythonBitmask").is_none() {
842 return Ok(());
843 }
844 let mut body = Vec::new();
845 for bit in bits {
846 let Ok(bit_node) = ast.bit_value(*bit) else {
847 return Err(MaterialiseError::Unevaluated {
848 construct: "bitmask bit the model listed".to_string(),
849 at: node.name.clone(),
850 });
851 };
852 if ghost_created_for(applied, bit_node.node_id, "PythonBitConst").is_none() {
853 continue;
854 }
855 let position = ast.effective_bit_position(*bit).ok_or_else(|| {
856 MaterialiseError::Unevaluated {
857 construct: "bit position".to_string(),
858 at: bit_node.name.clone(),
859 }
860 })?;
861 // `1 << 40` is an `int` shift in Python and it is 2^40;
862 // the Java column needs `1L` here because `1 << 40` is
863 // an int shift there and evaluates to 256.
864 body.push(Statement::Assign(Assign {
865 doc: renamed_note(&bit_node.name),
866 name: Ident::new(&escape_name(&bit_node.name)),
867 value: Expr::Binary {
868 op: "<<",
869 lhs: Box::new(Expr::int(1)),
870 rhs: Box::new(Expr::int(i128::from(position))),
871 },
872 }));
873 }
874 items.push(Pending {
875 module: node.module,
876 statement: enum_class(
877 &node.name,
878 "IntFlag",
879 body,
880 Doc::lines(&[
881 "A bitmask. An enum.IntFlag member is an int and combines with |,",
882 "which is what a bitmask is for.",
883 "",
884 "A member of this type is annotated int: a writer may set a bit",
885 "this declaration does not name.",
886 ]),
887 ),
888 });
889 }
890
891 TypeKind::Bitset { .. } => {
892 if ghost_created_for(applied, node.node_id, "PythonBitset").is_none() {
893 return Ok(());
894 }
895 let mut body = Vec::new();
896 // The base's bitfields are fields of the derived class too:
897 // an inherited bitset is one flat run.
898 for bitfield in &ast.all_bitfields_of(id) {
899 let Ok(field_node) = ast.bitfield(*bitfield) else {
900 return Err(MaterialiseError::Unevaluated {
901 construct: "bitset field the model listed".to_string(),
902 at: node.name.clone(),
903 });
904 };
905 // An anonymous field is padding: it moves the next field
906 // along and carries no value of its own.
907 let Some(field_name) = field_node.name.as_ref() else {
908 continue;
909 };
910 body.push(field(field_name, bit_width(field_node.bits), false));
911 }
912 items.push(Pending {
913 module: node.module,
914 statement: dataclass(
915 &node.name,
916 body,
917 Doc::lines(&[
918 "A bitset. Each field states its own bit width in the metadata;",
919 "Python has no fixed-width integer, so nothing is rounded up to",
920 "a holder the way the C and Java columns have to.",
921 "",
922 "The packing into the anchor is the codec's.",
923 ]),
924 ),
925 });
926 }
927
928 TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
929 let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
930 "PythonValue"
931 } else {
932 "PythonStruct"
933 };
934 if ghost_created_for(applied, node.node_id, created).is_none() {
935 return Ok(());
936 }
937 // XTypes 1.3 §7.5.1.4.1.3: the base is the first member `parent`, an
938 // instance of the base's dataclass, and not Python inheritance: a dataclass
939 // that inherited would also inherit the base's decorator arguments, which is
940 // a second place the shape is decided. The wire still carries the base's
941 // members first (§7.4.1.1.5.1).
942 //
943 // Every member is emitted, with no per-member rule lookup: a
944 // member whose lookup missed would vanish from the class and
945 // the file would still import. The C and Java columns take
946 // the same view.
947 let mut body = Vec::new();
948 let mut spelled = Vec::new();
949 if let Some(base) = super::c::struct_base(ast, id) {
950 spelled.push((escape_name("parent"), "parent".to_string()));
951 body.push(field(
952 "parent",
953 python_member_type(ast, base, "parent")?,
954 false,
955 ));
956 }
957 for member in ast.members_of(id).iter().copied() {
958 let Ok(member_node) = ast.member(member) else {
959 return Err(MaterialiseError::Unevaluated {
960 construct: "member the model listed".to_string(),
961 at: node.name.clone(),
962 });
963 };
964 let Some(target) = member_node.type_.resolved() else {
965 return Err(MaterialiseError::Unevaluated {
966 construct: "member type".to_string(),
967 at: member_node.name.clone(),
968 });
969 };
970 spelled.push((escape_name(&member_node.name), member_node.name.clone()));
971 body.push(field(
972 &member_node.name,
973 python_member_type(ast, target, &member_node.name)?,
974 member_node.facets.is_optional(),
975 ));
976 }
977 // `check_scope` catches this too, at the end, over the
978 // rendered statements — but by then the IDL names are gone
979 // and it can only name the spelling the two members share.
980 // Here both are still in hand.
981 no_spelling_collision(
982 spelled
983 .iter()
984 .map(|(field, declared)| (field.as_str(), declared.as_str())),
985 "Python",
986 &node.name,
987 )?;
988 items.push(Pending {
989 module: node.module,
990 statement: dataclass(&node.name, body, Doc::none()),
991 });
992 }
993
994 TypeKind::Union { discriminator, .. } => {
995 if ghost_created_for(applied, node.node_id, "PythonUnion").is_none() {
996 return Ok(());
997 }
998 let Some(disc) = discriminator.resolved() else {
999 return Err(MaterialiseError::Unevaluated {
1000 construct: "union discriminator".to_string(),
1001 at: node.name.clone(),
1002 });
1003 };
1004 // A field per case and not one per type: two cases may carry
1005 // the same type, and Python has no untagged union to overlay
1006 // them with. Which field is live is what `_d` says.
1007 let mut body = vec![Statement::Annotated(Annotated {
1008 doc: Doc::line("The discriminator. Which case field is live is what this says."),
1009 name: Ident::new("_d"),
1010 type_: python_member_type(ast, disc, &node.name)?,
1011 value: None,
1012 })];
1013 for case in ast.cases_of(id) {
1014 let Ok(case_node) = ast.case(*case) else {
1015 return Err(MaterialiseError::Unevaluated {
1016 construct: "union case the model listed".to_string(),
1017 at: node.name.clone(),
1018 });
1019 };
1020 if ghost_created_for(applied, case_node.node_id, "PythonCase").is_none() {
1021 continue;
1022 }
1023 let Ok(member) = ast.member(case_node.member) else {
1024 return Err(MaterialiseError::Unevaluated {
1025 construct: "member behind a union case the model listed".to_string(),
1026 at: node.name.clone(),
1027 });
1028 };
1029 let Some(target) = member.type_.resolved() else {
1030 return Err(MaterialiseError::Unevaluated {
1031 construct: "union case type".to_string(),
1032 at: member.name.clone(),
1033 });
1034 };
1035 // Optional and defaulting to None, because at most one
1036 // case is live: a required field per case would make
1037 // constructing the union mean supplying every branch.
1038 body.push(field(
1039 &member.name,
1040 python_member_type(ast, target, &member.name)?,
1041 true,
1042 ));
1043 }
1044 items.push(Pending {
1045 module: node.module,
1046 statement: dataclass(
1047 &node.name,
1048 body,
1049 Doc::lines(&[
1050 "A union: the discriminator, and one field per case.",
1051 "",
1052 "Every case field is Optional and defaults to None, because at",
1053 "most one of them is live. Which one is what _d says; Python has",
1054 "no way to state that in a type.",
1055 ]),
1056 ),
1057 });
1058 }
1059
1060 // A native type is opaque and has no Python declaration — a
1061 // comment records the name and what a use of it takes.
1062 TypeKind::Native => {
1063 if ghost_created_for(applied, node.node_id, "PythonNative").is_none() {
1064 return Ok(());
1065 }
1066 items.push(Pending {
1067 module: node.module,
1068 statement: Statement::Comment(Doc::line(&format!(
1069 "native {}; -- every use takes {}.{}",
1070 node.name,
1071 zerodds_lang_render::python::type_(&native_type()),
1072 native_note(ast, id),
1073 ))),
1074 });
1075 }
1076
1077 // A value box is a reference in Python already: the type, or
1078 // None. Nothing to declare.
1079 TypeKind::ValueBox { .. } => {}
1080
1081 // An object reference belongs to the RPC arc and an annotation
1082 // type has no runtime form at all — `zerodds_ast_layout` refuses
1083 // to plan either — so a Python declaration would be a shape
1084 // nothing could serialise. `any` and `ValueBase` are in the
1085 // model because Building Block Any is one of IDL 4.2's fifteen,
1086 // and what they hold is decided at run time. All four are named
1087 // here rather than passed over, and `python_member_type` refuses
1088 // them by name where a member would carry one.
1089 TypeKind::ObjectRef { .. }
1090 | TypeKind::AnnotationType { .. }
1091 | TypeKind::Any
1092 | TypeKind::ValueBase => {}
1093 }
1094 Ok(())
1095}
1096
1097/// The Python annotation of a value in a position that has to hold one,
1098/// refusing the constructs Python cannot hold.
1099///
1100/// `any` is the first of those. What it holds is decided at run time —
1101/// there is no Python type for it and no wire form to generate a codec
1102/// against. Emitting `a: object` into the class and reporting success
1103/// produces a field no encoder can write, from a generator that said
1104/// nothing was wrong. Python makes this refusal harder than the other
1105/// four columns do, because `object` would type-check.
1106///
1107/// # Errors
1108/// [`MaterialiseError::NoLanguageForm`] for `any`, `ValueBase`, an object
1109/// reference and an annotation type;
1110/// [`MaterialiseError::Unevaluated`] for a type or a bound the model will
1111/// not answer for.
1112pub fn python_member_type(
1113 ast: &Ast,
1114 target: Id<TypeNode>,
1115 at: &str,
1116) -> Result<Type, MaterialiseError> {
1117 type_at(ast, target, at, 0)
1118}
1119
1120/// [`python_member_type`], carrying how deep the spelling has nested.
1121///
1122/// zerodds-lint: recursion-depth 64 (collection and typedef nesting;
1123/// bounded by `MAX_TYPE_DEPTH`, which returns
1124/// `MaterialiseError::Unevaluated` naming the spelling that nested)
1125#[allow(clippy::too_many_lines)]
1126fn type_at(ast: &Ast, id: Id<TypeNode>, at: &str, depth: usize) -> Result<Type, MaterialiseError> {
1127 if depth > MAX_TYPE_DEPTH {
1128 return Err(MaterialiseError::Unevaluated {
1129 construct: format!("type, whose Python spelling nests past {MAX_TYPE_DEPTH}"),
1130 at: at.to_string(),
1131 });
1132 }
1133 let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1134 construct: "type".to_string(),
1135 at: at.to_string(),
1136 })?;
1137 let refused = match &node.kind {
1138 TypeKind::Any => Some("any"),
1139 TypeKind::ValueBase => Some("ValueBase"),
1140 // An object reference is a handle into the RPC arc and
1141 // `coverage.rs` says so; there is no Python data type for it.
1142 TypeKind::ObjectRef { .. } => Some("Object"),
1143 // An annotation type has no runtime form, so nothing declares
1144 // one and a reference to it would name a class that is not
1145 // there.
1146 TypeKind::AnnotationType { .. } => Some("annotation type"),
1147 _ => None,
1148 };
1149 if let Some(construct) = refused {
1150 return Err(MaterialiseError::NoLanguageForm {
1151 construct: construct.to_string(),
1152 language: "Python",
1153 at: at.to_string(),
1154 });
1155 }
1156 Ok(match &node.kind {
1157 TypeKind::Primitive(kind) => alias(primitive_alias(*kind)),
1158 TypeKind::String { width, bound } => annotated(
1159 Type::name("str"),
1160 call(
1161 "_IdlText",
1162 vec![
1163 kw("bits", Expr::int(char_bits(*width))),
1164 kw("bound", Expr::int(bound_of(ast, *bound, at)?)),
1165 ],
1166 ),
1167 ),
1168 TypeKind::Fixed { digits, scale } => annotated(
1169 Type::name("str"),
1170 call(
1171 "_IdlFixed",
1172 vec![
1173 kw("digits", Expr::int(i128::from(*digits))),
1174 kw("scale", Expr::int(i128::from(*scale))),
1175 ],
1176 ),
1177 ),
1178 TypeKind::Sequence { element, bound } => {
1179 let Some(element) = element.resolved() else {
1180 return Err(MaterialiseError::Unevaluated {
1181 construct: "sequence element".to_string(),
1182 at: at.to_string(),
1183 });
1184 };
1185 let list = Type::subscript(
1186 Path::single("list"),
1187 vec![type_at(ast, element, at, depth + 1)?],
1188 );
1189 match bound_of(ast, *bound, at)? {
1190 0 => list,
1191 length => annotated(
1192 list,
1193 call("_IdlSequence", vec![kw("bound", Expr::int(length))]),
1194 ),
1195 }
1196 }
1197 TypeKind::Array { element, dims } => {
1198 let Some(element) = element.resolved() else {
1199 return Err(MaterialiseError::Unevaluated {
1200 construct: "array element".to_string(),
1201 at: at.to_string(),
1202 });
1203 };
1204 let mut out = type_at(ast, element, at, depth + 1)?;
1205 // Innermost dimension first, so the outer one ends up
1206 // outermost: `long a[2][3]` is two rows of three.
1207 for dim in dims.iter().rev() {
1208 let length =
1209 ast.eval_integral(*dim)
1210 .map_err(|_| MaterialiseError::Unevaluated {
1211 construct: "array length".to_string(),
1212 at: at.to_string(),
1213 })?;
1214 let length =
1215 usize::try_from(length).map_err(|_| MaterialiseError::NoLanguageForm {
1216 construct: format!("an array of {length} elements"),
1217 language: "Python",
1218 at: at.to_string(),
1219 })?;
1220 // One tuple argument per element, so a static checker
1221 // rejects a literal of the wrong length. `tuple[T, ...]`
1222 // would say only that it is a tuple.
1223 out = Type::subscript(Path::single("tuple"), vec![out; length]);
1224 }
1225 out
1226 }
1227 TypeKind::Map { key, value, bound } => {
1228 let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
1229 return Err(MaterialiseError::Unevaluated {
1230 construct: "map key or value".to_string(),
1231 at: at.to_string(),
1232 });
1233 };
1234 let entry = Type::subscript(
1235 Path::single("tuple"),
1236 vec![
1237 type_at(ast, key, at, depth + 1)?,
1238 type_at(ast, value, at, depth + 1)?,
1239 ],
1240 );
1241 let list = Type::subscript(Path::single("list"), vec![entry]);
1242 match bound_of(ast, *bound, at)? {
1243 0 => list,
1244 length => annotated(
1245 list,
1246 call("_IdlSequence", vec![kw("bound", Expr::int(length))]),
1247 ),
1248 }
1249 }
1250 // A typedef is transparent: the use takes the target's type.
1251 TypeKind::Alias { aliased } => {
1252 let Some(target) = aliased.resolved() else {
1253 return Err(MaterialiseError::Unevaluated {
1254 construct: "typedef target".to_string(),
1255 at: at.to_string(),
1256 });
1257 };
1258 type_at(ast, target, at, depth + 1)?
1259 }
1260 // An `int`, with the enumeration named in the metadata. See the
1261 // module documentation: `Colour(7)` raises, and an @appendable
1262 // writer sends exactly that.
1263 TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => annotated(
1264 Type::name("int"),
1265 call(
1266 "_IdlEnum",
1267 vec![
1268 kw("declared", Expr::Path(qualified(ast, id)?)),
1269 kw("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
1270 ],
1271 ),
1272 ),
1273 TypeKind::Structure { .. }
1274 | TypeKind::Union { .. }
1275 | TypeKind::Bitset { .. }
1276 | TypeKind::Valuetype { .. } => Type::Name(qualified(ast, id)?),
1277 TypeKind::Native => native_type(),
1278 // A boxed value is a reference, absent when None — which is what
1279 // `typing.Optional` says.
1280 TypeKind::ValueBox { boxed: inner } => {
1281 let Some(target) = inner.resolved() else {
1282 return Err(MaterialiseError::Unevaluated {
1283 construct: "value box target".to_string(),
1284 at: at.to_string(),
1285 });
1286 };
1287 optional(type_at(ast, target, at, depth + 1)?)
1288 }
1289 TypeKind::Any
1290 | TypeKind::ValueBase
1291 | TypeKind::ObjectRef { .. }
1292 | TypeKind::AnnotationType { .. } => {
1293 // Refused above; the arm keeps the match total, so a kind
1294 // added to the model without a Python answer is a compile
1295 // error and not a field that silently takes another's type.
1296 return Err(MaterialiseError::NoLanguageForm {
1297 construct: node.kind.as_str().to_string(),
1298 language: "Python",
1299 at: at.to_string(),
1300 });
1301 }
1302 })
1303}
1304
1305/// The alias an IDL primitive takes.
1306///
1307/// Exhaustive, so a primitive added to the model without a Python
1308/// spelling is a compile error and not a member that silently takes
1309/// another's.
1310#[must_use]
1311pub const fn primitive_alias(kind: PrimitiveKind) -> &'static str {
1312 match kind {
1313 PrimitiveKind::Boolean => "bool",
1314 PrimitiveKind::Char => CHAR,
1315 PrimitiveKind::WChar => WCHAR,
1316 PrimitiveKind::Int8 => "_Int8",
1317 PrimitiveKind::Octet | PrimitiveKind::UInt8 => "_UInt8",
1318 PrimitiveKind::Int16 => "_Int16",
1319 PrimitiveKind::UInt16 => "_UInt16",
1320 PrimitiveKind::Int32 => INT32,
1321 PrimitiveKind::UInt32 => "_UInt32",
1322 PrimitiveKind::Int64 => "_Int64",
1323 PrimitiveKind::UInt64 => "_UInt64",
1324 PrimitiveKind::Float32 => "_Float32",
1325 PrimitiveKind::Float64 => "_Float64",
1326 // A `str` holding the decimal, and never a Python `float`: see
1327 // the module documentation.
1328 PrimitiveKind::Float128 => "_Float128",
1329 }
1330}
1331
1332/// The alias `long` takes, which is also what an unresolved constant
1333/// falls back to.
1334const INT32: &str = "_Int32";
1335/// The alias an IDL `char` takes.
1336const CHAR: &str = "_Char";
1337/// The alias an IDL `wchar` takes.
1338const WCHAR: &str = "_WChar";
1339
1340/// How wide one character of `width` is on the wire.
1341const fn char_bits(width: CharWidth) -> i128 {
1342 match width {
1343 CharWidth::Char => 8,
1344 CharWidth::WChar => 16,
1345 }
1346}
1347
1348/// A bound as a number, with `0` meaning unbounded.
1349///
1350/// # Errors
1351/// [`MaterialiseError::Unevaluated`] when the bounding expression does
1352/// not evaluate. Zero is what unbounded is written as, so a bound that
1353/// failed to evaluate must not fall back to it: the two would be
1354/// indistinguishable in the output.
1355fn bound_of(ast: &Ast, bound: Bound, at: &str) -> Result<i128, MaterialiseError> {
1356 match bound.expr() {
1357 None => Ok(0),
1358 Some(expr) => ast
1359 .eval_integral(expr)
1360 .map_err(|_| MaterialiseError::Unevaluated {
1361 construct: "bound".to_string(),
1362 at: at.to_string(),
1363 }),
1364 }
1365}
1366
1367/// What a `native` declaration maps to.
1368///
1369/// `object`, always. A `#pragma` or an annotation may give a mapping
1370/// spelling, and it is written into the comment rather than into the
1371/// annotation: a Python annotation naming a class this file does not
1372/// import would not resolve, and a generated module imports only
1373/// `dataclasses`, `enum` and `typing`.
1374fn native_type() -> Type {
1375 Type::name("object")
1376}
1377
1378/// The mapping a `native` declaration named, as a clause of its comment.
1379fn native_note(ast: &Ast, id: Id<TypeNode>) -> String {
1380 let mapping = ast
1381 .any_of_type(id)
1382 .ok()
1383 .and_then(|any| ast.facets(any))
1384 .and_then(|facets| facets.corba.as_ref())
1385 .and_then(|corba| corba.native_mapping.clone());
1386 match mapping {
1387 Some(spelling) => format!(
1388 " The source named the mapping `{spelling}`, which this module does not import."
1389 ),
1390 None => String::new(),
1391 }
1392}
1393
1394/// A generated type's path, written from the module root down.
1395///
1396/// `a.b.Point` and not `Point`: a nested module is a class, and a class
1397/// body is not a scope an annotation is resolved against.
1398///
1399/// # Errors
1400/// [`MaterialiseError::NoModule`] when the model will not answer for a
1401/// module the type nests in.
1402fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
1403 let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
1404 construct: "type".to_string(),
1405 at: "the module root".to_string(),
1406 })?;
1407 let mut segments = Vec::new();
1408 for (_, name) in module_chain(ast, node.module)? {
1409 segments.push(name);
1410 }
1411 segments.push(escape_name(&node.name));
1412 Ok(Path::new(
1413 &segments.iter().map(String::as_str).collect::<Vec<&str>>(),
1414 ))
1415}
1416
1417/// The path a generated declaration of `id` is reached by.
1418///
1419/// `qualified` under a public name, because the codec backend needs it
1420/// and is in a different module. A codec sits at the file root, where a
1421/// class nested in `class a: class b:` is `a.b.Point` and `Point` names
1422/// nothing — the same reason the annotations this file writes are
1423/// qualified.
1424///
1425/// # Errors
1426/// As `qualified`.
1427pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
1428 qualified(ast, id)
1429}
1430
1431/// The bound a `string`, a `sequence` or a `map` carries, as the suffix
1432/// that keeps two of them apart.
1433///
1434/// The tag names the routine a hoisted collection's codec is emitted
1435/// under, and one body is emitted per name. With the bound left out,
1436/// `sequence<long>` and `sequence<long, 4>` are one name and whichever
1437/// was declared first decides: measured on
1438/// `@final struct S { sequence<long> b; sequence<long, 4> a; };`, the
1439/// generated Python held no `zd_over` check at all, so `a` accepted a
1440/// stream of any length; with the members the other way round both got
1441/// the bounded body, so `b` refused — and truncated — a length the IDL
1442/// puts no limit on. `string<4>` beside `string` and `wstring<4>`
1443/// beside `wstring` collapse the same way, onto the trimming call.
1444/// Either way the reader answered with a value the IDL does not
1445/// describe and said nothing, which is why the bound is part of the
1446/// name. The same suffix the Zig column carries.
1447///
1448/// An `array` needs nothing here: its `dims` are already in the tag,
1449/// under the `Array` arm below.
1450fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
1451 match bound.expr() {
1452 None => String::new(),
1453 // `_b` and not a bare `_n`: `seq_long_4` is what the `Array`
1454 // arm answers for an array of four `long`, and the two are
1455 // different wire forms.
1456 Some(expr) => match ast.eval_integral(expr) {
1457 Ok(limit) => format!("_b{limit}"),
1458 // As the array's `_x`: a bound that does not evaluate is
1459 // still not the unbounded form, and the materialiser
1460 // reports the expression that would not evaluate.
1461 Err(_) => "_bx".to_string(),
1462 },
1463 }
1464}
1465
1466/// The name a codec function takes for the type it codes.
1467///
1468/// The **IDL kind** and not the Python spelling, and in this column that
1469/// distinction does more work than in any other: `octet`, `unsigned
1470/// short`, `long` and `unsigned long long` are all `int` in Python, and
1471/// they are one, two, four and eight octets on the wire. A tag built
1472/// from the Python type would give every one of them the same codec, and
1473/// the first type reached would decide the width for the rest with
1474/// nothing to say it had.
1475///
1476/// An alias is followed through: two IDL names for one shape are one
1477/// wire form, and two codecs for it would be two chances to differ.
1478///
1479/// # Errors
1480/// [`MaterialiseError`] when a module will not answer, or when the name
1481/// nests past `MAX_TYPE_DEPTH`.
1482///
1483/// zerodds-lint: recursion-depth 64 (collection nesting; bounded by `MAX_TYPE_DEPTH`)
1484pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
1485 if depth > MAX_TYPE_DEPTH {
1486 return Err(MaterialiseError::Unevaluated {
1487 construct: format!("type, whose codec name nests past {MAX_TYPE_DEPTH}"),
1488 at: "the module root".to_string(),
1489 });
1490 }
1491 let Ok(node) = ast.type_(id) else {
1492 return Ok("unknown".to_string());
1493 };
1494 Ok(match &node.kind {
1495 TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
1496 TypeKind::String {
1497 width: CharWidth::Char,
1498 bound,
1499 } => format!("string{}", bound_tag(ast, bound)),
1500 TypeKind::String {
1501 width: CharWidth::WChar,
1502 bound,
1503 } => format!("wstring{}", bound_tag(ast, bound)),
1504 TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
1505 TypeKind::Sequence { element, bound } => match element.resolved() {
1506 Some(element) => format!(
1507 "seq{}_{}",
1508 bound_tag(ast, bound),
1509 type_tag(ast, element, depth + 1)?
1510 ),
1511 None => format!("seq_unknown{}", bound_tag(ast, bound)),
1512 },
1513 TypeKind::Array { element, dims } => {
1514 let mut name = match element.resolved() {
1515 Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)?),
1516 None => "arr_unknown".to_string(),
1517 };
1518 for dim in dims {
1519 match ast.eval_integral(*dim) {
1520 Ok(length) => name.push_str(&format!("_{length}")),
1521 // No number rather than a zero: the name has to
1522 // differ from a `[0]` array's, and `declare` reports
1523 // the dimension that would not evaluate by name.
1524 Err(_) => name.push_str("_x"),
1525 }
1526 }
1527 name
1528 }
1529 TypeKind::Map { key, value, bound } => {
1530 let half = |half: Option<Id<TypeNode>>| match half {
1531 Some(target) => type_tag(ast, target, depth + 1),
1532 None => Ok("unknown".to_string()),
1533 };
1534 format!(
1535 "map{}_{}_{}",
1536 bound_tag(ast, bound),
1537 half(key.resolved())?,
1538 half(value.resolved())?
1539 )
1540 }
1541 TypeKind::Alias { aliased } => match aliased.resolved() {
1542 Some(target) => type_tag(ast, target, depth + 1)?,
1543 None => "unknown".to_string(),
1544 },
1545 TypeKind::ValueBox { boxed } => match boxed.resolved() {
1546 Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
1547 None => "box_unknown".to_string(),
1548 },
1549 TypeKind::Structure { .. }
1550 | TypeKind::Union { .. }
1551 | TypeKind::Enumeration { .. }
1552 | TypeKind::Bitmask { .. }
1553 | TypeKind::Bitset { .. }
1554 | TypeKind::Valuetype { .. }
1555 | TypeKind::Native => {
1556 let mut segments = Vec::new();
1557 for (_, name) in module_chain(ast, node.module)? {
1558 segments.push(name);
1559 }
1560 segments.push(escape_name(&node.name));
1561 segments.join("_")
1562 }
1563 TypeKind::Any => "any".to_string(),
1564 TypeKind::ValueBase => "valuebase".to_string(),
1565 TypeKind::ObjectRef { .. } => "objectref".to_string(),
1566 TypeKind::AnnotationType { .. } => "annotation".to_string(),
1567 })
1568}
1569
1570// -- building the tree ----------------------------------------------------
1571
1572/// `name = typing.Annotated[…]`, referred to by its alias.
1573fn alias(name: &'static str) -> Type {
1574 Type::name(name)
1575}
1576
1577/// `typing.Annotated[base, marker]`.
1578fn annotated(base: Type, marker: Expr) -> Type {
1579 Type::Annotated {
1580 base: Box::new(base),
1581 markers: vec![marker],
1582 }
1583}
1584
1585/// `typing.Optional[inner]`.
1586fn optional(inner: Type) -> Type {
1587 Type::Optional(Box::new(inner))
1588}
1589
1590/// `name(args)`.
1591fn call(name: &str, args: Vec<Arg>) -> Expr {
1592 Expr::Call {
1593 func: Path::single(name),
1594 args,
1595 }
1596}
1597
1598/// `name=value`, as a call argument.
1599fn kw(name: &str, value: Expr) -> Arg {
1600 Arg::Keyword(Keyword {
1601 name: Ident::new(name),
1602 value,
1603 })
1604}
1605
1606/// The metadata of an integer of `bits` bits, unsigned.
1607///
1608/// What a bitset's bitfield takes. The declared holder of
1609/// `bitfield<1, boolean>` names a range and not a wire form, and the
1610/// width is what the packing uses.
1611fn bit_width(bits: u16) -> Type {
1612 annotated(
1613 Type::name("int"),
1614 call(
1615 "_IdlInt",
1616 vec![
1617 kw("bits", Expr::int(i128::from(bits))),
1618 kw("signed", Expr::Literal(Literal::Bool(false))),
1619 ],
1620 ),
1621 )
1622}
1623
1624/// One dataclass field.
1625///
1626/// An `@optional` member defaults to `None` and a required one has no
1627/// default at all, so constructing an aggregate without a required
1628/// member is an error rather than a silently zero-filled sample.
1629///
1630/// A member whose name was escaped carries the IDL name in
1631/// `dataclasses.field(metadata=…)`, which is machine-readable — a
1632/// comment would not be.
1633fn field(name: &str, type_: Type, is_optional: bool) -> Statement {
1634 let escaped = escape_name(name);
1635 let renamed = escaped != name;
1636 let type_ = if is_optional { optional(type_) } else { type_ };
1637 let value = match (is_optional, renamed) {
1638 (false, false) => None,
1639 (true, false) => Some(Expr::Literal(Literal::Null)),
1640 (_, true) => {
1641 let mut args = Vec::new();
1642 if is_optional {
1643 args.push(kw("default", Expr::Literal(Literal::Null)));
1644 }
1645 args.push(kw(
1646 "metadata",
1647 Expr::Dict(vec![(
1648 Expr::Literal(Literal::string("idl")),
1649 Expr::Literal(Literal::string(name)),
1650 )]),
1651 ));
1652 Some(Expr::Call {
1653 func: Path::new(&["dataclasses", "field"]),
1654 args,
1655 })
1656 }
1657 };
1658 Statement::Annotated(Annotated {
1659 doc: Doc::none(),
1660 name: Ident::new(&escaped),
1661 type_,
1662 value,
1663 })
1664}
1665
1666/// `@dataclasses.dataclass(kw_only=True) class <name>: …`
1667///
1668/// `kw_only=True` because the field order is the IDL's and the defaults
1669/// are not: an `@optional` member defaults to `None` and the member
1670/// after it does not, and a positional dataclass refuses a field with no
1671/// default after one with a default. Reordering the fields to satisfy
1672/// that would make the class's field order differ from the IDL's, which
1673/// is the order the wire uses.
1674///
1675/// Mutable and comparing by value: the defaults. A frozen dataclass
1676/// could not be filled in as a sample is decoded, and `eq=True` is what
1677/// makes two decodes of one sample compare equal, which is what a
1678/// round-trip test asserts.
1679fn dataclass(name: &str, body: Vec<Statement>, doc: Doc) -> Statement {
1680 let escaped = escape_name(name);
1681 let mut doc = doc;
1682 if escaped != name && doc.is_empty() {
1683 doc = Doc::line(&format!("IDL name: {name}"));
1684 } else if escaped != name {
1685 doc.push("");
1686 doc.push(&format!("IDL name: {name}"));
1687 }
1688 Statement::Class(Class {
1689 doc,
1690 decorators: vec![Decorator {
1691 path: Path::new(&["dataclasses", "dataclass"]),
1692 args: vec![kw("kw_only", Expr::Literal(Literal::Bool(true)))],
1693 }],
1694 name: Ident::new(&escaped),
1695 bases: Vec::new(),
1696 body: if body.is_empty() {
1697 vec![Statement::Pass]
1698 } else {
1699 body
1700 },
1701 })
1702}
1703
1704/// `class <name>(enum.<base>): …`
1705fn enum_class(name: &str, base: &str, body: Vec<Statement>, doc: Doc) -> Statement {
1706 let escaped = escape_name(name);
1707 let mut doc = doc;
1708 if escaped != name {
1709 doc.push("");
1710 doc.push(&format!("IDL name: {name}"));
1711 }
1712 Statement::Class(Class {
1713 doc,
1714 decorators: Vec::new(),
1715 name: Ident::new(&escaped),
1716 bases: vec![Type::Name(Path::new(&["enum", base]))],
1717 // An IDL enumeration with no enumerators is legal in the model
1718 // and a class with an empty body is not legal Python.
1719 body: if body.is_empty() {
1720 vec![Statement::Pass]
1721 } else {
1722 body
1723 },
1724 })
1725}
1726
1727/// The comment a renamed declaration carries, and nothing when it kept
1728/// its name.
1729fn renamed_note(name: &str) -> Doc {
1730 if escape_name(name) == name {
1731 return Doc::none();
1732 }
1733 Doc::line(&format!("IDL name: {name}"))
1734}
1735
1736// -- the preamble ---------------------------------------------------------
1737
1738/// The marker classes and the width aliases every generated module
1739/// carries.
1740///
1741/// The same in every file, whether or not a given alias is used. Two
1742/// reasons, and the second is the one that decides it:
1743///
1744/// - the preamble is the vocabulary the rest of the file is written in,
1745/// so a reader learns it once and a diff between two generated modules
1746/// is only the declarations;
1747/// - it is the contract with the codec pass. A module that dropped
1748/// `_IdlFixed` because no member happened to be a `fixed` would make
1749/// `getattr(module, "_IdlFixed")` succeed or fail depending on the
1750/// IDL, which is a dependency on the input that nothing declares.
1751///
1752/// The markers are per-module classes, so `_IdlInt` in two generated
1753/// files are two classes. That is deliberate: a shared one would make
1754/// every generated module import a runtime package, and this layer emits
1755/// types and nothing else.
1756fn prelude() -> Vec<Statement> {
1757 let mut out = vec![
1758 marker(
1759 "_IdlInt",
1760 &[("bits", "int"), ("signed", "bool")],
1761 Doc::lines(&[
1762 "The IDL integer an int annotation stands for.",
1763 "",
1764 "A Python int is arbitrary precision and an IDL long is 32 bits.",
1765 "Nothing in Python enforces the difference, so it is stated here",
1766 "instead: read it back with typing.get_type_hints(cls,",
1767 "include_extras=True).",
1768 ]),
1769 ),
1770 marker(
1771 "_IdlFloat",
1772 &[("bits", "int")],
1773 Doc::lines(&[
1774 "The IDL floating-point type a float or str annotation stands for.",
1775 "",
1776 "bits=128 goes with a str and not a float: a Python float is a",
1777 "binary64 and would drop sixty bits of the significand at the",
1778 "assignment.",
1779 ]),
1780 ),
1781 marker(
1782 "_IdlText",
1783 &[("bits", "int"), ("bound", "int")],
1784 Doc::lines(&[
1785 "An IDL string, wstring, char or wchar, as a Python str.",
1786 "",
1787 "bits is the width of one character on the wire, 8 or 16. bound is",
1788 "the declared maximum length, or 0 for an unbounded one.",
1789 ]),
1790 ),
1791 marker(
1792 "_IdlFixed",
1793 &[("digits", "int"), ("scale", "int")],
1794 Doc::lines(&[
1795 "An IDL fixed<digits, scale>, as the decimal text.",
1796 "",
1797 "Not a decimal.Decimal: Decimal('0.00') keeps one digit and a",
1798 "scale, so the digit count the FixedDigitCount axis measures would",
1799 "be the container's answer rather than the source's.",
1800 ]),
1801 ),
1802 marker(
1803 "_IdlSequence",
1804 &[("bound", "int")],
1805 Doc::line("The declared maximum length of a bounded sequence or map."),
1806 ),
1807 marker(
1808 "_IdlEnum",
1809 &[("declared", "type[enum.Enum]"), ("bits", "int")],
1810 Doc::lines(&[
1811 "The enumeration or bitmask an int annotation stands for.",
1812 "",
1813 "The member is an int because a writer may send a value this",
1814 "declaration does not list, which enum.IntEnum would refuse.",
1815 ]),
1816 ),
1817 ];
1818 for (name, bits, signed) in [
1819 ("_Int8", 8, true),
1820 ("_UInt8", 8, false),
1821 ("_Int16", 16, true),
1822 ("_UInt16", 16, false),
1823 ("_Int32", 32, true),
1824 ("_UInt32", 32, false),
1825 ("_Int64", 64, true),
1826 ("_UInt64", 64, false),
1827 ] {
1828 out.push(Statement::Assign(Assign {
1829 doc: Doc::none(),
1830 name: Ident::new(name),
1831 value: type_value(annotated(
1832 Type::name("int"),
1833 call(
1834 "_IdlInt",
1835 vec![
1836 kw("bits", Expr::int(bits)),
1837 kw("signed", Expr::Literal(Literal::Bool(signed))),
1838 ],
1839 ),
1840 )),
1841 }));
1842 }
1843 for (name, base, bits) in [
1844 ("_Float32", "float", 32),
1845 ("_Float64", "float", 64),
1846 // A binary128 in a Python float would lose sixty bits of the
1847 // significand at the assignment; the decimal text loses nothing.
1848 ("_Float128", "str", 128),
1849 ] {
1850 out.push(Statement::Assign(Assign {
1851 doc: Doc::none(),
1852 name: Ident::new(name),
1853 value: type_value(annotated(
1854 Type::name(base),
1855 call("_IdlFloat", vec![kw("bits", Expr::int(bits))]),
1856 )),
1857 }));
1858 }
1859 for (name, bits) in [(CHAR, 8), (WCHAR, 16)] {
1860 out.push(Statement::Assign(Assign {
1861 doc: Doc::none(),
1862 name: Ident::new(name),
1863 value: type_value(annotated(
1864 Type::name("str"),
1865 call(
1866 "_IdlText",
1867 vec![kw("bits", Expr::int(bits)), kw("bound", Expr::int(1))],
1868 ),
1869 )),
1870 }));
1871 }
1872 out
1873}
1874
1875/// One frozen marker dataclass.
1876fn marker(name: &str, fields: &[(&str, &str)], doc: Doc) -> Statement {
1877 Statement::Class(Class {
1878 doc,
1879 decorators: vec![Decorator {
1880 path: Path::new(&["dataclasses", "dataclass"]),
1881 args: vec![kw("frozen", Expr::Literal(Literal::Bool(true)))],
1882 }],
1883 name: Ident::new(name),
1884 bases: Vec::new(),
1885 body: fields
1886 .iter()
1887 .map(|(field_name, type_name)| {
1888 Statement::Annotated(Annotated {
1889 doc: Doc::none(),
1890 name: Ident::new(field_name),
1891 type_: Type::Name(Path::new(&type_name.split('.').collect::<Vec<&str>>())),
1892 value: None,
1893 })
1894 })
1895 .collect(),
1896 })
1897}
1898
1899/// A type written where a value belongs, which is what a type alias is.
1900fn type_value(type_: Type) -> Expr {
1901 Expr::Annotation(Box::new(type_))
1902}
1903
1904// -- constants ------------------------------------------------------------
1905
1906/// The IDL primitive a type resolves to, when it resolves to one.
1907///
1908/// What a constant's range check needs. A `const long` has to fit 32
1909/// bits whatever Python's `int` could hold, because the wire is 32 bits
1910/// wide.
1911fn scalar_kind(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
1912 let mut current = id;
1913 for _ in 0..MAX_TYPE_DEPTH {
1914 let node = ast.type_(current).ok()?;
1915 match &node.kind {
1916 TypeKind::Primitive(kind) => return Some(*kind),
1917 TypeKind::Alias { aliased } => current = aliased.resolved()?,
1918 _ => return None,
1919 }
1920 }
1921 None
1922}
1923
1924/// The Python expression a constant's value takes.
1925///
1926/// The declared type already carries the string-shaped forms: a
1927/// `string`, a `wstring`, a `fixed` and a `long double` are all a Python
1928/// `str` here. What this has to do is pick the right literal for each,
1929/// and refuse a value the IDL type does not hold.
1930///
1931/// **A range check even though Python needs none.** A Python `int` holds
1932/// 2^70 perfectly well, and `const octet X = 300;` would compile and
1933/// then be written as one byte. So the range is checked against the
1934/// **IDL** type and not against Python's, which is what keeps this
1935/// column's failure set comparable with the other four's.
1936///
1937/// # Errors
1938/// [`MaterialiseError`] naming the constant and what will not hold it.
1939fn constant_expr(
1940 kind: Option<PrimitiveKind>,
1941 value: &zerodds_ast::ConstValue,
1942 at: &str,
1943) -> Result<Expr, MaterialiseError> {
1944 use zerodds_ast::ConstValue;
1945 let refuse = |construct: String| MaterialiseError::NoLanguageForm {
1946 construct,
1947 language: "Python",
1948 at: at.to_string(),
1949 };
1950 match value {
1951 ConstValue::Integer(number) => {
1952 let (low, high, name) = integer_range(kind);
1953 Ok(Expr::int(in_range(*number, low, high, name, at)?))
1954 }
1955 ConstValue::Boolean(flag) => Ok(Expr::Literal(Literal::Bool(*flag))),
1956 // Python has no character type, so both are a string of one.
1957 // Unlike Java there is no code point a Python `str` cannot hold,
1958 // so a `wchar` above the Basic Multilingual Plane is written
1959 // rather than refused.
1960 ConstValue::Char(character) => Ok(Expr::Literal(Literal::Char(*character))),
1961 ConstValue::WChar(point) => match constant::wchar_char(*point) {
1962 Some(character) => Ok(Expr::Literal(Literal::Char(character))),
1963 None => Err(refuse(format!(
1964 "the code point U+{point:04X}, which is not a scalar value"
1965 ))),
1966 },
1967 ConstValue::Float { mantissa, exponent } => {
1968 let text = constant::float_text(mantissa, exponent.as_deref());
1969 // A `long double` constant is the decimal text, because the
1970 // member that holds one is a `str`. Parsing it into a Python
1971 // `float` here would round it at the point of generation.
1972 if matches!(kind, Some(PrimitiveKind::Float128)) {
1973 return Ok(Expr::Literal(Literal::Str(text)));
1974 }
1975 Ok(Expr::Literal(Literal::Float(text)))
1976 }
1977 ConstValue::Fixed { digits, scale } => Ok(Expr::Literal(Literal::Str(
1978 constant::fixed_decimal(digits, *scale),
1979 ))),
1980 ConstValue::String(text) => Ok(Expr::Literal(Literal::Str(text.clone()))),
1981 ConstValue::WString(points) => match constant::wstring_utf8(points) {
1982 Some(text) => Ok(Expr::Literal(Literal::Str(text))),
1983 None => Err(refuse(String::from(
1984 "a `wstring` constant holding a lone surrogate",
1985 ))),
1986 },
1987 // `ConstValue` is `#[non_exhaustive]`, so a form added to the
1988 // model reaches here. Refused by name rather than given a
1989 // plausible default: a constant emitted as the wrong literal is
1990 // a value that disagrees with the IDL and imports cleanly.
1991 _ => Err(refuse(String::from(
1992 "a constant form this binding has no literal for",
1993 ))),
1994 }
1995}
1996
1997/// The range an integer constant of `kind` has to fit, and the name to
1998/// report when it does not.
1999///
2000/// A type the model would not resolve falls back to the widest IDL
2001/// integer rather than to Python's unbounded one: something has to bound
2002/// it, and 64 bits is the widest thing the wire carries.
2003const fn integer_range(kind: Option<PrimitiveKind>) -> (i128, i128, &'static str) {
2004 match kind {
2005 Some(PrimitiveKind::Int8) => (-128, 127, "int8"),
2006 Some(PrimitiveKind::Octet | PrimitiveKind::UInt8) => (0, 255, "octet"),
2007 Some(PrimitiveKind::Int16) => (i16::MIN as i128, i16::MAX as i128, "short"),
2008 Some(PrimitiveKind::UInt16) => (0, u16::MAX as i128, "unsigned short"),
2009 Some(PrimitiveKind::Int32) => (i32::MIN as i128, i32::MAX as i128, "long"),
2010 Some(PrimitiveKind::UInt32) => (0, u32::MAX as i128, "unsigned long"),
2011 Some(PrimitiveKind::UInt64) => (0, u64::MAX as i128, "unsigned long long"),
2012 Some(PrimitiveKind::Char) => (0, 255, "char"),
2013 Some(PrimitiveKind::WChar) => (0, u16::MAX as i128, "wchar"),
2014 Some(PrimitiveKind::Boolean) => (0, 1, "boolean"),
2015 _ => (i64::MIN as i128, i64::MAX as i128, "long long"),
2016 }
2017}
2018
2019/// `value`, or the failure that says it does not fit.
2020///
2021/// # Errors
2022/// [`MaterialiseError::NoLanguageForm`], naming the value and the IDL
2023/// type that will not hold it. Truncating instead would put a number
2024/// nobody wrote into the output — and Python would not even notice,
2025/// which is why this check is here and not left to the language.
2026fn in_range(
2027 value: i128,
2028 low: i128,
2029 high: i128,
2030 type_: &str,
2031 at: &str,
2032) -> Result<i128, MaterialiseError> {
2033 if value < low || value > high {
2034 return Err(MaterialiseError::NoLanguageForm {
2035 construct: format!("the value {value}, which an IDL `{type_}` does not hold"),
2036 language: "Python",
2037 at: at.to_string(),
2038 });
2039 }
2040 Ok(value)
2041}
2042
2043/// A model node's name, or its identity when the node is anonymous.
2044///
2045/// An array type declared inline has no name of its own, and an error
2046/// that said only "the array length does not evaluate" would leave the
2047/// reader to find which array.
2048fn named(name: &str, node: NodeId) -> String {
2049 if name.is_empty() {
2050 format!("{node:?}")
2051 } else {
2052 name.to_string()
2053 }
2054}
2055
2056/// Whether the rules created a node of `type_name` for `node`.
2057///
2058/// The same two-hop walk the sibling materialisers do: a correspondence
2059/// is an ordinary node with a link to the anchor and a link to what was
2060/// created.
2061fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
2062 let anchor = applied
2063 .origin
2064 .iter()
2065 .find(|(_, model)| **model == node)
2066 .map(|(ghost, _)| *ghost)?;
2067 let typ = applied.graph.types.lookup(type_name)?;
2068 applied
2069 .graph
2070 .nodes_of_type(typ)
2071 .map(|created| created.id)
2072 .find(|created| {
2073 applied.graph.connected(&anchor, created)
2074 || applied
2075 .graph
2076 .parts(&anchor)
2077 .any(|part| applied.graph.connected(&part.other, created))
2078 })
2079}
2080
2081#[cfg(test)]
2082mod tests {
2083 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2084 use super::*;
2085 use crate::rule;
2086 use crate::{ModelGraph, apply};
2087 use std::path::{Path as FsPath, PathBuf};
2088
2089 /// IDL in, one Python module out.
2090 fn render(idl: &str, path: &str) -> Result<String, String> {
2091 let parsed = zerodds_idl_parse::parse(idl);
2092 if parsed.has_errors() {
2093 return Err(format!("parse: {:?}", parsed.diagnostics()));
2094 }
2095 let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
2096 if !lowered.diagnostics.is_empty() {
2097 return Err(format!("lower: {:?}", lowered.diagnostics));
2098 }
2099 let mut ast = lowered.ast;
2100 ast.resolve_all_by_name()
2101 .map_err(|error| format!("resolve: {error}"))?;
2102 let applied = apply(&ModelGraph::of(&ast), &rule::python::catalogue())
2103 .map_err(|error| format!("apply: {error}"))?;
2104 let module =
2105 python_module(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
2106 Ok(zerodds_lang_render::python::module(&module))
2107 }
2108
2109 /// The same, for a source with no file behind it.
2110 fn declarations(idl: &str) -> String {
2111 match render(idl, "test.idl") {
2112 Ok(text) => text,
2113 Err(why) => panic!("{why}"),
2114 }
2115 }
2116
2117 /// Every corpus case, by name.
2118 fn corpus() -> Vec<(String, PathBuf)> {
2119 let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
2120 let mut out = Vec::new();
2121 for entry in std::fs::read_dir(&root).expect("the corpus directory") {
2122 let entry = entry.expect("a corpus entry");
2123 let idl = entry.path().join("type.idl");
2124 if idl.is_file() {
2125 out.push((entry.file_name().to_string_lossy().into_owned(), idl));
2126 }
2127 }
2128 out.sort();
2129 out
2130 }
2131
2132 /// The whole corpus, through every layer, with nothing skipped.
2133 ///
2134 /// A rendered file is not a compiled one. `ZD_PYTHON_OUT` names a
2135 /// directory to write the modules into, and `python3 -m py_compile`
2136 /// plus `mypy --strict` over that directory is the second half of
2137 /// the check — a module that does not compile or does not
2138 /// type-check is a materialiser defect, not a corpus defect.
2139 #[test]
2140 fn every_corpus_case_renders_or_names_its_failure() {
2141 let cases = corpus();
2142 assert!(
2143 cases.len() > 300,
2144 "the corpus is not where it was: {} cases",
2145 cases.len()
2146 );
2147 let out = std::env::var("ZD_PYTHON_OUT").ok();
2148 let mut failures = Vec::new();
2149 for (name, path) in cases {
2150 let source = std::fs::read_to_string(&path).expect("a corpus source");
2151 match render(&source, &path.display().to_string()) {
2152 Ok(text) => {
2153 if let Some(dir) = &out {
2154 let case = FsPath::new(dir).join(&name);
2155 std::fs::create_dir_all(&case).expect("the output directory");
2156 std::fs::write(case.join("zd_types.py"), text).expect("the output file");
2157 }
2158 }
2159 Err(why) => failures.push(format!("{name}: {why}")),
2160 }
2161 }
2162 let failures = crate::adapter::without_the_front_end_refusals(failures);
2163 assert!(failures.is_empty(), "{failures:#?}");
2164 }
2165
2166 #[test]
2167 fn every_idl_primitive_reaches_an_annotation_that_states_its_width() {
2168 // The decision this column turns on: a Python `int` is
2169 // arbitrary precision, so the width has to be somewhere, and
2170 // `x: int` is never written.
2171 let generated = declarations(
2172 "@final struct P {
2173 boolean a; octet b; char c; wchar d;
2174 int8 e; uint8 f; short g; unsigned short h;
2175 long i; unsigned long j; long long k; unsigned long long l;
2176 float m; double n; long double o;
2177 };",
2178 );
2179 for expected in [
2180 "a: bool",
2181 "b: _UInt8",
2182 "c: _Char",
2183 "d: _WChar",
2184 "e: _Int8",
2185 "f: _UInt8",
2186 "g: _Int16",
2187 "h: _UInt16",
2188 "i: _Int32",
2189 "j: _UInt32",
2190 "k: _Int64",
2191 "l: _UInt64",
2192 "m: _Float32",
2193 "n: _Float64",
2194 "o: _Float128",
2195 ] {
2196 assert!(generated.contains(expected), "{expected}\n{generated}");
2197 }
2198 // Scoped to the aggregate: the marker dataclasses in the
2199 // preamble are the one place a bare `int` belongs.
2200 let body = generated
2201 .split_once("class P:")
2202 .map(|(_, rest)| rest.to_string())
2203 .expect("the aggregate");
2204 assert!(
2205 !body.contains(": int\n"),
2206 "a bare int reached a field\n{body}"
2207 );
2208 assert!(
2209 !body.contains(": float\n"),
2210 "a bare float reached a field\n{body}"
2211 );
2212 assert!(
2213 !body.contains(": str\n"),
2214 "a bare str reached a field\n{body}"
2215 );
2216 }
2217
2218 #[test]
2219 fn every_width_alias_states_its_bits_and_its_signedness() {
2220 let generated = declarations("@final struct P { long x; };");
2221 assert!(
2222 generated.contains("_Int32 = typing.Annotated[int, _IdlInt(bits=32, signed=True)]"),
2223 "{generated}"
2224 );
2225 assert!(
2226 generated.contains("_UInt64 = typing.Annotated[int, _IdlInt(bits=64, signed=False)]"),
2227 "{generated}"
2228 );
2229 }
2230
2231 #[test]
2232 fn a_long_double_is_the_decimal_text_and_never_a_python_float() {
2233 // A Python float is a binary64. Writing a binary128 into one
2234 // drops sixty bits of the significand at the assignment, before
2235 // any codec runs.
2236 let generated = declarations("@final struct P { long double x; };");
2237 assert!(
2238 generated.contains("_Float128 = typing.Annotated[str, _IdlFloat(bits=128)]"),
2239 "{generated}"
2240 );
2241 assert!(generated.contains("x: _Float128"), "{generated}");
2242 }
2243
2244 #[test]
2245 fn a_sequence_is_a_list_and_an_array_is_a_tuple_of_its_length() {
2246 let generated = declarations(
2247 "@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
2248 );
2249 assert!(generated.contains("s: list[_Int32]"), "{generated}");
2250 assert!(
2251 generated.contains("a: tuple[_Int32, _Int32, _Int32]"),
2252 "{generated}"
2253 );
2254 // Outer dimension outermost: two rows of three.
2255 assert!(
2256 generated
2257 .contains("g: tuple[tuple[_Int32, _Int32, _Int32], tuple[_Int32, _Int32, _Int32]]"),
2258 "{generated}"
2259 );
2260 assert!(
2261 generated.contains("t: list[typing.Annotated[str,"),
2262 "{generated}"
2263 );
2264 }
2265
2266 #[test]
2267 fn a_bound_reaches_the_annotation_and_an_unbounded_one_leaves_it_alone() {
2268 let generated = declarations(
2269 "@final struct P { sequence<long, 5> s; sequence<long> u; string<7> t; };",
2270 );
2271 assert!(
2272 generated.contains("s: typing.Annotated[list[_Int32], _IdlSequence(bound=5)]"),
2273 "{generated}"
2274 );
2275 assert!(generated.contains("u: list[_Int32]"), "{generated}");
2276 assert!(
2277 generated.contains("t: typing.Annotated[str, _IdlText(bits=8, bound=7)]"),
2278 "{generated}"
2279 );
2280 }
2281
2282 #[test]
2283 fn a_map_is_a_list_of_pairs_and_never_a_dict() {
2284 // A generated struct is an `eq=True` dataclass, so its
2285 // `__hash__` is None and `dict[Point, int]` is a type that
2286 // cannot hold anything. And the entry order is what the
2287 // MapEntryOrder axis measures.
2288 let generated = declarations("@final struct P { map<long, string> m; };");
2289 assert!(generated.contains("m: list[tuple[_Int32,"), "{generated}");
2290 assert!(!generated.contains("dict["), "{generated}");
2291 }
2292
2293 #[test]
2294 fn an_optional_member_is_optional_and_defaults_to_none() {
2295 // XTypes 1.3 7.4.1.2.3 makes absence the value, not a zero.
2296 let generated =
2297 declarations("@mutable struct P { @optional long a; @optional string b; long c; };");
2298 assert!(
2299 generated.contains("a: typing.Optional[_Int32] = None"),
2300 "{generated}"
2301 );
2302 assert!(
2303 generated.contains("b: typing.Optional[typing."),
2304 "{generated}"
2305 );
2306 // A required member has no default at all, so constructing the
2307 // aggregate without it is an error rather than a silently
2308 // zero-filled sample.
2309 assert!(generated.contains("c: _Int32\n"), "{generated}");
2310 }
2311
2312 #[test]
2313 fn a_dataclass_is_keyword_only_because_the_defaults_are_not_in_idl_order() {
2314 // An @optional member defaults to None and the member after it
2315 // does not; a positional dataclass refuses that, and reordering
2316 // the fields would make the class disagree with the wire.
2317 let generated = declarations("@mutable struct P { @optional long a; long b; };");
2318 assert!(
2319 generated.contains("@dataclasses.dataclass(kw_only=True)"),
2320 "{generated}"
2321 );
2322 }
2323
2324 #[test]
2325 fn an_enumeration_is_an_intenum_and_a_member_of_it_is_an_int() {
2326 let generated = declarations(
2327 "enum Colour { RED, GREEN, BLUE };
2328 @final struct P { Colour c; };",
2329 );
2330 assert!(
2331 generated.contains("class Colour(enum.IntEnum):"),
2332 "{generated}"
2333 );
2334 assert!(generated.contains("RED = 0"), "{generated}");
2335 assert!(generated.contains("BLUE = 2"), "{generated}");
2336 // An @appendable writer may send a value this IDL does not
2337 // list, and `Colour(7)` raises ValueError.
2338 assert!(
2339 generated.contains("c: typing.Annotated[int, _IdlEnum(declared=Colour, bits=32)]"),
2340 "{generated}"
2341 );
2342 }
2343
2344 #[test]
2345 fn an_explicit_enumerator_value_is_written_as_the_source_wrote_it() {
2346 // The Java column has to fold 70000 onto the pattern an `int`
2347 // occupies; a Python `int` holds it, so nothing folds.
2348 let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
2349 assert!(generated.contains("LOW = -1"), "{generated}");
2350 assert!(generated.contains("HIGH = 70000"), "{generated}");
2351 }
2352
2353 #[test]
2354 fn a_bitmask_is_an_intflag_and_a_wide_shift_needs_no_suffix() {
2355 let generated =
2356 declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
2357 assert!(
2358 generated.contains("class Big(enum.IntFlag):"),
2359 "{generated}"
2360 );
2361 // `1 << 40` is 2^40 in Python. The Java column needs `1L` here,
2362 // because there `1 << 40` is an int shift and evaluates to 256.
2363 assert!(generated.contains("B = (1 << 40)"), "{generated}");
2364 }
2365
2366 #[test]
2367 fn a_bitset_states_the_declared_bit_width_and_rounds_nothing_up() {
2368 let generated = declarations(
2369 "bitset Packed { bitfield<3> low; bitfield<5> high; };
2370 @final struct P { Packed value; };",
2371 );
2372 assert!(
2373 generated.contains("low: typing.Annotated[int, _IdlInt(bits=3, signed=False)]"),
2374 "{generated}"
2375 );
2376 assert!(
2377 generated.contains("high: typing.Annotated[int, _IdlInt(bits=5, signed=False)]"),
2378 "{generated}"
2379 );
2380 assert!(generated.contains("value: Packed"), "{generated}");
2381 }
2382
2383 #[test]
2384 fn a_fixed_is_a_str_and_never_a_decimal() {
2385 // `Decimal("0.00").as_tuple().digits` is `(0,)` — one digit —
2386 // so a Decimal would answer the FixedDigitCount axis itself.
2387 let generated = declarations(
2388 "typedef fixed<9,3> Money;
2389 @final struct P { Money amount; };",
2390 );
2391 assert!(
2392 generated.contains("amount: typing.Annotated[str, _IdlFixed(digits=9, scale=3)]"),
2393 "{generated}"
2394 );
2395 // The preamble says in prose why a Decimal is not used; what
2396 // must not appear is an import of it or a member typed as one.
2397 assert!(!generated.contains("import decimal"), "{generated}");
2398 assert!(!generated.contains("amount: decimal"), "{generated}");
2399 }
2400
2401 #[test]
2402 fn a_union_is_a_dataclass_with_a_discriminator_and_one_optional_field_per_case() {
2403 let generated = declarations(
2404 "union Choice switch (long) {
2405 case 1: long whole;
2406 case 2: long other;
2407 default: double fraction;
2408 };
2409 @final struct P { Choice c; };",
2410 );
2411 assert!(generated.contains("_d: _Int32"), "{generated}");
2412 assert!(
2413 generated.contains("whole: typing.Optional[_Int32] = None"),
2414 "{generated}"
2415 );
2416 assert!(
2417 generated.contains("other: typing.Optional[_Int32] = None"),
2418 "{generated}"
2419 );
2420 assert!(
2421 generated.contains("fraction: typing.Optional[_Float64] = None"),
2422 "{generated}"
2423 );
2424 assert!(generated.contains("c: Choice"), "{generated}");
2425 }
2426
2427 #[test]
2428 fn a_typedef_declares_nothing_and_says_why() {
2429 // An eagerly evaluated Python alias could not be ordered
2430 // against the classes it names.
2431 let generated = declarations(
2432 "typedef sequence<long> Row;
2433 @final struct P { Row r; };",
2434 );
2435 assert!(generated.contains("r: list[_Int32]"), "{generated}");
2436 assert!(
2437 generated.contains("typedef list[_Int32] Row;"),
2438 "{generated}"
2439 );
2440 }
2441
2442 #[test]
2443 fn a_module_is_a_nested_class_and_a_reference_is_written_from_the_root() {
2444 let generated = declarations(
2445 "module a { @final struct P { long x; }; };
2446 module b { @final struct P { double y; }; };
2447 @final struct Q { a::P one; b::P two; };",
2448 );
2449 assert!(generated.contains("class a:"), "{generated}");
2450 assert!(generated.contains("class b:"), "{generated}");
2451 assert!(generated.contains("one: a.P"), "{generated}");
2452 assert!(generated.contains("two: b.P"), "{generated}");
2453 }
2454
2455 #[test]
2456 fn a_module_reopened_beside_a_nested_one_is_one_class() {
2457 // `module common { struct Robot; module util { … } }` — the
2458 // arrangement that produced two classes of one name when the
2459 // grouping was by module id rather than by path. Python binds a
2460 // name rather than declaring one, so the second would have
2461 // silently replaced the first.
2462 let generated = declarations(
2463 "module common {
2464 module util { @final struct Location { double x; }; };
2465 @final struct Robot { ::common::util::Location where_; };
2466 };",
2467 );
2468 assert_eq!(generated.matches("class common:").count(), 1, "{generated}");
2469 assert!(
2470 generated.contains("where_: common.util.Location"),
2471 "{generated}"
2472 );
2473 }
2474
2475 #[test]
2476 fn an_identifier_that_is_a_python_keyword_is_escaped_and_the_idl_name_is_kept() {
2477 let generated = declarations(
2478 "module class {
2479 enum type { match, lambda, virtual };
2480 @final struct P { long global; };
2481 };",
2482 );
2483 assert!(generated.contains("class class_:"), "{generated}");
2484 assert!(
2485 generated.contains("class type_(enum.IntEnum):"),
2486 "{generated}"
2487 );
2488 assert!(generated.contains("match_ = 0"), "{generated}");
2489 assert!(generated.contains("lambda_ = 1"), "{generated}");
2490 // Not a Python keyword, so it keeps its spelling.
2491 assert!(generated.contains("virtual = 2"), "{generated}");
2492 // The IDL name survives the escape, machine-readably for a
2493 // field and in a comment for everything else.
2494 assert!(
2495 generated
2496 .contains("global_: _Int32 = dataclasses.field(metadata={\"idl\": \"global\"})"),
2497 "{generated}"
2498 );
2499 assert!(generated.contains("IDL name: match"), "{generated}");
2500 }
2501
2502 #[test]
2503 fn a_member_named_after_a_builtin_the_annotations_use_is_escaped_too() {
2504 // The case the parser does not catch. `int: typing.Optional[…] =
2505 // None` sets a class attribute called `int`, and
2506 // `typing.get_type_hints` resolves the class namespace before
2507 // the module's — so every `int` in that class would resolve to
2508 // `None`.
2509 let generated = declarations("@mutable struct P { @optional long int; long list; };");
2510 assert!(generated.contains("int_:"), "{generated}");
2511 assert!(generated.contains("list_:"), "{generated}");
2512 }
2513
2514 #[test]
2515 fn two_names_that_would_escape_onto_one_are_refused() {
2516 // Python binds a name; it does not declare one. The second
2517 // `class_` would silently replace the first.
2518 let failure = render("@final struct P { long class; long class_; };", "test.idl")
2519 .expect_err("two members escaping onto one name are refused");
2520 assert!(failure.contains("class_"), "{failure}");
2521 }
2522
2523 #[test]
2524 fn a_construct_with_no_python_data_form_is_named_and_not_left_out() {
2525 // `object` would type-check for all of these, which is exactly
2526 // why it is not written: a member typed `object` is a field no
2527 // codec can write, from a generator that reported success.
2528 for (idl, construct) in [
2529 ("@final struct P { any a; long tail; };", "any"),
2530 ("@final struct P { Object o; long tail; };", "Object"),
2531 ] {
2532 let failure = render(idl, "test.idl").expect_err(construct);
2533 assert!(failure.contains(construct), "{failure}");
2534 assert!(failure.contains("Python"), "{failure}");
2535 }
2536 }
2537
2538 #[test]
2539 fn a_constant_outside_its_idl_range_is_refused_although_python_would_hold_it() {
2540 // A Python `int` holds 300 perfectly well and the wire holds one
2541 // byte. Nothing in the language would notice.
2542 let failure = render("const octet X = 300;", "test.idl")
2543 .expect_err("an octet constant of 300 is refused");
2544 assert!(failure.contains("300"), "{failure}");
2545 assert!(failure.contains("octet"), "{failure}");
2546 }
2547
2548 #[test]
2549 fn a_constant_is_final_and_carries_its_idl_type() {
2550 let generated = declarations("const long MAX = 5; const string NAME = \"z\";");
2551 assert!(
2552 generated.contains("MAX: typing.Final[_Int32] = 5"),
2553 "{generated}"
2554 );
2555 assert!(
2556 generated.contains("NAME: typing.Final[typing.Annotated[str,"),
2557 "{generated}"
2558 );
2559 }
2560
2561 #[test]
2562 fn an_unevaluated_array_length_is_named_and_not_substituted() {
2563 // The C and Java columns fail on the same input; the columns'
2564 // failure sets stay comparable.
2565 let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
2566 .expect_err("an array whose length does not evaluate is refused");
2567 assert!(!failure.is_empty(), "{failure}");
2568 }
2569
2570 #[test]
2571 fn an_interface_declares_nothing_and_the_rest_of_the_file_still_renders() {
2572 // The one rule in the catalogue whose node nothing reads. An
2573 // interface has no data form in a types-only column, so the
2574 // check is that it is passed over rather than that it breaks
2575 // the file — coverage.rs is where the cell says so.
2576 let generated = declarations(
2577 "interface Sensor { long read(); };
2578 @final struct P { long x; };",
2579 );
2580 assert!(generated.contains("class P:"), "{generated}");
2581 assert!(!generated.contains("Sensor"), "{generated}");
2582 }
2583
2584 #[test]
2585 fn an_empty_aggregate_still_has_a_body() {
2586 // A class header with no block under it is a SyntaxError, not
2587 // untidy output.
2588 let generated = declarations("@final struct P { };");
2589 assert!(generated.contains("class P:"), "{generated}");
2590 assert!(generated.contains("pass"), "{generated}");
2591 }
2592}