1use std::collections::BTreeSet;
12use std::fmt::Write as _;
13
14use zerodds_idl::ast::types::{
15 BitmaskDecl, BitsetDecl, CaseLabel, ConstExpr, ConstrTypeDecl, Declarator, Definition, EnumDef,
16 ExceptDecl, FloatingType, Identifier, IntegerType, Literal, LiteralKind, Member, ModuleDef,
17 PrimitiveType, ScopedName, SequenceType, Specification, StringType, StructDcl, StructDef,
18 SwitchTypeSpec, TypeDecl, TypeSpec, TypedefDecl, UnaryOp, UnionDcl, UnionDef,
19};
20
21use crate::error::{IdlPythonError, Result};
22
23#[derive(Debug, Clone, Default)]
25pub struct PythonGenOptions {
26 pub header_comment: Option<String>,
29}
30
31pub fn generate_python_module(spec: &Specification, opts: &PythonGenOptions) -> Result<String> {
40 let mut imports = ImportSet::default();
41 collect_imports(&spec.definitions, &mut imports)?;
42
43 let mut out = String::new();
44 out.push_str("# SPDX-License-Identifier: Apache-2.0\n");
45 if let Some(c) = &opts.header_comment {
46 for line in c.lines() {
47 out.push_str("# ");
48 out.push_str(line);
49 out.push('\n');
50 }
51 }
52 out.push_str("# Auto-generated by `zerodds-idl-python`. Do not edit by hand.\n");
53 out.push('\n');
54
55 imports.emit(&mut out);
56
57 let mut scope: Vec<String> = Vec::new();
58 emit_definitions(&mut out, &spec.definitions, &mut scope)?;
59 Ok(out)
60}
61
62#[derive(Default)]
67struct ImportSet {
68 dataclass: bool,
69 int_enum: bool,
70 int_flag: bool,
71 typing_list: bool,
72 typing_type_alias: bool,
73 zerodds_brands: BTreeSet<&'static str>,
74 zerodds_idl_struct: bool,
75 zerodds_idl_union: bool,
76}
77
78impl ImportSet {
79 fn emit(&self, out: &mut String) {
80 if self.dataclass {
81 out.push_str("from dataclasses import dataclass\n");
82 }
83 if self.int_enum || self.int_flag {
84 out.push_str("from enum import ");
85 let mut parts: Vec<&str> = Vec::new();
86 if self.int_enum {
87 parts.push("IntEnum");
88 }
89 if self.int_flag {
90 parts.push("IntFlag");
91 }
92 out.push_str(&parts.join(", "));
93 out.push('\n');
94 }
95 if self.typing_list || self.typing_type_alias {
96 out.push_str("from typing import ");
97 let mut parts: Vec<&str> = Vec::new();
98 if self.typing_list {
99 parts.push("List");
100 }
101 if self.typing_type_alias {
102 parts.push("TypeAlias");
103 }
104 out.push_str(&parts.join(", "));
105 out.push('\n');
106 }
107 if self.zerodds_idl_struct || self.zerodds_idl_union || !self.zerodds_brands.is_empty() {
108 out.push_str("from zerodds.idl import ");
109 let mut parts: Vec<String> = Vec::new();
110 if self.zerodds_idl_struct {
111 parts.push("idl_struct".into());
112 }
113 if self.zerodds_idl_union {
114 parts.push("idl_union".into());
115 }
116 for brand in &self.zerodds_brands {
117 parts.push((*brand).to_string());
118 }
119 out.push_str(&parts.join(", "));
120 out.push('\n');
121 }
122 out.push('\n');
123 }
124}
125
126fn collect_imports(defs: &[Definition], imports: &mut ImportSet) -> Result<()> {
128 for d in defs {
129 match d {
130 Definition::Module(m) => collect_imports(&m.definitions, imports)?,
131 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
132 imports.dataclass = true;
133 imports.zerodds_idl_struct = true;
134 for m in &s.members {
135 collect_type_imports(&m.type_spec, imports)?;
136 }
137 }
138 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(_))) => {
139 imports.int_enum = true;
140 }
141 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(_))) => {
142 imports.int_flag = true;
143 }
144 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(_))) => {
145 imports.zerodds_brands.insert("Int64");
149 }
150 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
151 imports.zerodds_idl_union = true;
152 imports.int_enum = true;
153 collect_switch_type_imports(&u.switch_type, imports);
154 for case in &u.cases {
155 collect_type_imports(&case.element.type_spec, imports)?;
156 }
157 }
158 Definition::Type(TypeDecl::Typedef(td)) => {
159 imports.typing_type_alias = true;
160 collect_type_imports(&td.type_spec, imports)?;
161 }
162 Definition::Except(e) => {
163 imports.dataclass = true;
164 imports.zerodds_idl_struct = true;
165 for m in &e.members {
166 collect_type_imports(&m.type_spec, imports)?;
167 }
168 }
169 _ => {
170 }
175 }
176 }
177 Ok(())
178}
179
180fn collect_type_imports(ts: &TypeSpec, imports: &mut ImportSet) -> Result<()> {
182 match ts {
183 TypeSpec::Primitive(p) => {
184 imports.zerodds_brands.insert(brand_for_primitive(*p));
185 }
186 TypeSpec::String(s) => {
187 imports.zerodds_brands.insert(brand_for_string(s));
188 }
189 TypeSpec::Sequence(seq) => {
190 imports.typing_list = true;
191 collect_type_imports(&seq.elem, imports)?;
192 }
193 TypeSpec::Scoped(_) => {
194 }
197 TypeSpec::Fixed(_) | TypeSpec::Map(_) | TypeSpec::Any => {
198 return Err(IdlPythonError::Unsupported(format!(
199 "type spec not yet supported in python codegen: {ts:?}"
200 )));
201 }
202 }
203 Ok(())
204}
205
206fn collect_switch_type_imports(switch: &SwitchTypeSpec, imports: &mut ImportSet) {
207 match switch {
208 SwitchTypeSpec::Integer(i) => {
209 imports.zerodds_brands.insert(brand_for_integer(*i));
210 }
211 SwitchTypeSpec::Boolean => {
212 }
215 SwitchTypeSpec::Octet => {
216 imports.zerodds_brands.insert("Octet");
217 }
218 SwitchTypeSpec::Char => {
219 imports.zerodds_brands.insert("Char");
220 }
221 SwitchTypeSpec::Scoped(_) => {
222 }
225 }
226}
227
228fn emit_definitions(out: &mut String, defs: &[Definition], scope: &mut Vec<String>) -> Result<()> {
234 for d in defs {
235 match d {
236 Definition::Module(m) => emit_module(out, m, scope)?,
237 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
238 emit_struct(out, s, scope)?;
239 }
240 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Forward(_)))) => {
241 }
243 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
244 emit_enum(out, e, scope)?;
245 }
246 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
247 emit_bitmask(out, b, scope)?;
248 }
249 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
250 emit_bitset(out, b, scope)?;
251 }
252 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
253 emit_union(out, u, scope)?;
254 }
255 Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Forward(_)))) => {
256 }
258 Definition::Type(TypeDecl::Typedef(td)) => {
259 emit_typedef(out, td, scope)?;
260 }
261 Definition::Except(e) => {
262 emit_exception(out, e, scope)?;
263 }
264 _ => {
265 return Err(IdlPythonError::Unsupported(format!(
266 "definition variant not yet supported: {d:?}"
267 )));
268 }
269 }
270 }
271 Ok(())
272}
273
274fn emit_module(out: &mut String, m: &ModuleDef, scope: &mut Vec<String>) -> Result<()> {
276 scope.push(m.name.text.clone());
277 emit_definitions(out, &m.definitions, scope)?;
278 scope.pop();
279 Ok(())
280}
281
282fn emit_struct(out: &mut String, s: &StructDef, scope: &[String]) -> Result<()> {
283 let typename = qualified_typename(&s.name, scope);
284 let class_name = python_class_name(&s.name, scope);
285
286 writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
287 writeln!(out, "@dataclass").ok();
288 if let Some(base) = &s.base {
289 let base_class = python_scoped_name(base);
290 writeln!(out, "class {class_name}({base_class}):").ok();
291 } else {
292 writeln!(out, "class {class_name}:").ok();
293 }
294
295 if s.members.is_empty() && s.base.is_none() {
296 writeln!(out, " pass").ok();
297 } else if s.members.is_empty() {
298 writeln!(out, " pass").ok();
301 } else {
302 for m in &s.members {
303 emit_members_of(out, m, scope)?;
304 }
305 }
306 out.push('\n');
307 Ok(())
308}
309
310fn emit_exception(out: &mut String, e: &ExceptDecl, scope: &[String]) -> Result<()> {
311 let typename = qualified_typename(&e.name, scope);
312 let class_name = python_class_name(&e.name, scope);
313
314 writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
315 writeln!(out, "@dataclass").ok();
316 writeln!(out, "class {class_name}(Exception):").ok();
317
318 if e.members.is_empty() {
319 writeln!(out, " pass").ok();
320 } else {
321 for m in &e.members {
322 emit_members_of(out, m, scope)?;
323 }
324 }
325 out.push('\n');
326 Ok(())
327}
328
329fn emit_members_of(out: &mut String, m: &Member, scope: &[String]) -> Result<()> {
330 let py_type = python_type_for(&m.type_spec, scope)?;
331 for d in &m.declarators {
332 let field_name = escape_python_keyword(d.name().text.as_str());
333 match d {
334 Declarator::Simple(_) => {
335 writeln!(out, " {field_name}: {py_type}").ok();
336 }
337 Declarator::Array(arr) => {
338 let mut t = py_type.clone();
339 for _ in 0..arr.sizes.len() {
340 t = format!("List[{t}]");
341 }
342 writeln!(out, " {field_name}: {t}").ok();
343 }
344 }
345 }
346 Ok(())
347}
348
349fn emit_enum(out: &mut String, e: &EnumDef, scope: &[String]) -> Result<()> {
350 let class_name = python_class_name(&e.name, scope);
351 writeln!(out, "class {class_name}(IntEnum):").ok();
352 if e.enumerators.is_empty() {
353 writeln!(out, " pass").ok();
354 } else {
355 for (idx, en) in e.enumerators.iter().enumerate() {
356 writeln!(out, " {} = {idx}", en.name.text).ok();
357 }
358 }
359 out.push('\n');
360 Ok(())
361}
362
363fn emit_bitmask(out: &mut String, b: &BitmaskDecl, scope: &[String]) -> Result<()> {
364 let class_name = python_class_name(&b.name, scope);
365 writeln!(out, "class {class_name}(IntFlag):").ok();
366 if b.values.is_empty() {
367 writeln!(out, " pass").ok();
368 } else {
369 for (idx, v) in b.values.iter().enumerate() {
370 writeln!(out, " {} = 1 << {idx}", v.name.text).ok();
371 }
372 }
373 out.push('\n');
374 Ok(())
375}
376
377fn emit_bitset(out: &mut String, b: &BitsetDecl, scope: &[String]) -> Result<()> {
378 let class_name = python_class_name(&b.name, scope);
388 writeln!(out, "{class_name}: TypeAlias = Int64").ok();
389 writeln!(out).ok();
390 writeln!(out, "class {class_name}_Bits:").ok();
391 if b.bitfields.is_empty() {
392 writeln!(out, " pass").ok();
393 } else {
394 let mut shift: u64 = 0;
395 for bf in &b.bitfields {
396 let width = eval_const_int(&bf.spec.width).ok_or_else(|| {
397 IdlPythonError::Unsupported(format!(
398 "bitset width is not a literal integer: {:?}",
399 bf.spec.width
400 ))
401 })?;
402 if width <= 0 {
403 return Err(IdlPythonError::Unsupported(format!(
404 "bitset width must be > 0, got {width}"
405 )));
406 }
407 let width = width as u64;
408 let mask = if width >= 64 {
409 u64::MAX
410 } else {
411 (1u64 << width) - 1
412 };
413 if let Some(name) = &bf.name {
414 let up = name.text.to_uppercase();
415 writeln!(out, " {up}_SHIFT = {shift}").ok();
416 writeln!(out, " {up}_WIDTH = {width}").ok();
417 writeln!(out, " {up}_MASK = 0x{mask:x}").ok();
418 }
419 shift += width;
420 }
421 }
422 out.push('\n');
423 Ok(())
424}
425
426fn emit_union(out: &mut String, u: &UnionDef, scope: &[String]) -> Result<()> {
427 let typename = qualified_typename(&u.name, scope);
428 let class_name = python_class_name(&u.name, scope);
429 let discriminator_repr = switch_type_python_repr(&u.switch_type);
430
431 let mut case_entries: Vec<(i64, String, String)> = Vec::new();
435 let mut default_entry: Option<(String, String)> = None;
436
437 for case in &u.cases {
438 let field_name = escape_python_keyword(case.element.declarator.name().text.as_str());
439 let py_type = python_type_for(&case.element.type_spec, scope)?;
440 for label in &case.labels {
441 match label {
442 CaseLabel::Value(expr) => {
443 let v = eval_const_int(expr).ok_or_else(|| {
444 IdlPythonError::Unsupported(format!(
445 "union case label is not a literal integer (scoped/enum-ref \
446 references will be supported in a follow-up): {expr:?}"
447 ))
448 })?;
449 case_entries.push((v, field_name.clone(), py_type.clone()));
450 }
451 CaseLabel::Default => {
452 default_entry = Some((field_name.clone(), py_type.clone()));
453 }
454 }
455 }
456 }
457
458 writeln!(out, "{class_name} = idl_union(").ok();
459 writeln!(out, " typename=\"{typename}\",").ok();
460 writeln!(out, " discriminator={discriminator_repr},").ok();
461 writeln!(out, " cases={{").ok();
462 for (label_value, field, py_type) in &case_entries {
463 writeln!(out, " {label_value}: (\"{field}\", {py_type}),").ok();
464 }
465 writeln!(out, " }},").ok();
466 if let Some((field, py_type)) = &default_entry {
467 writeln!(out, " default=(\"{field}\", {py_type}),").ok();
468 } else {
469 writeln!(out, " default=None,").ok();
470 }
471 writeln!(out, ")").ok();
472 out.push('\n');
473 Ok(())
474}
475
476fn emit_typedef(out: &mut String, td: &TypedefDecl, scope: &[String]) -> Result<()> {
477 let py_type = python_type_for(&td.type_spec, scope)?;
478 for d in &td.declarators {
479 let alias_name = python_class_name(d.name(), scope);
480 match d {
481 Declarator::Simple(_) => {
482 writeln!(out, "{alias_name}: TypeAlias = {py_type}").ok();
483 }
484 Declarator::Array(arr) => {
485 let mut t = py_type.clone();
486 for _ in 0..arr.sizes.len() {
487 t = format!("List[{t}]");
488 }
489 writeln!(out, "{alias_name}: TypeAlias = {t}").ok();
490 }
491 }
492 }
493 out.push('\n');
494 Ok(())
495}
496
497fn python_type_for(ts: &TypeSpec, scope: &[String]) -> Result<String> {
503 match ts {
504 TypeSpec::Primitive(p) => Ok(brand_for_primitive(*p).to_string()),
505 TypeSpec::String(s) => Ok(brand_for_string(s).to_string()),
506 TypeSpec::Sequence(seq) => {
507 let inner = python_type_for_seq_elem(seq, scope)?;
508 Ok(format!("List[{inner}]"))
509 }
510 TypeSpec::Scoped(name) => Ok(python_scoped_name(name)),
511 TypeSpec::Fixed(_) | TypeSpec::Map(_) | TypeSpec::Any => Err(IdlPythonError::Unsupported(
512 format!("type spec not yet supported in python codegen: {ts:?}"),
513 )),
514 }
515}
516
517fn python_type_for_seq_elem(seq: &SequenceType, scope: &[String]) -> Result<String> {
519 python_type_for(&seq.elem, scope)
520}
521
522fn brand_for_primitive(p: PrimitiveType) -> &'static str {
523 match p {
524 PrimitiveType::Boolean => "bool",
525 PrimitiveType::Octet => "Octet",
526 PrimitiveType::Char => "Char",
527 PrimitiveType::WideChar => "WChar",
528 PrimitiveType::Integer(i) => brand_for_integer(i),
529 PrimitiveType::Floating(f) => match f {
530 FloatingType::Float => "Float32",
531 FloatingType::Double => "Float64",
532 FloatingType::LongDouble => "LongDouble",
533 },
534 }
535}
536
537fn brand_for_integer(i: IntegerType) -> &'static str {
538 match i {
539 IntegerType::Short | IntegerType::Int16 => "Int16",
540 IntegerType::UShort | IntegerType::UInt16 => "UInt16",
541 IntegerType::Long | IntegerType::Int32 => "Int32",
542 IntegerType::ULong | IntegerType::UInt32 => "UInt32",
543 IntegerType::LongLong | IntegerType::Int64 => "Int64",
544 IntegerType::ULongLong | IntegerType::UInt64 => "UInt64",
545 IntegerType::Int8 => "Int8",
546 IntegerType::UInt8 => "UInt8",
547 }
548}
549
550fn brand_for_string(s: &StringType) -> &'static str {
551 if s.wide { "WString" } else { "String" }
552}
553
554fn switch_type_python_repr(switch: &SwitchTypeSpec) -> String {
555 match switch {
556 SwitchTypeSpec::Integer(i) => brand_for_integer(*i).to_string(),
557 SwitchTypeSpec::Boolean => "bool".to_string(),
558 SwitchTypeSpec::Octet => "Octet".to_string(),
559 SwitchTypeSpec::Char => "Char".to_string(),
560 SwitchTypeSpec::Scoped(s) => python_scoped_name(s),
561 }
562}
563
564fn python_scoped_name(name: &ScopedName) -> String {
565 name.parts
566 .iter()
567 .map(|p| p.text.clone())
568 .collect::<Vec<_>>()
569 .join("_")
570}
571
572fn qualified_typename(name: &Identifier, scope: &[String]) -> String {
573 if scope.is_empty() {
574 name.text.clone()
575 } else {
576 format!("{}::{}", scope.join("::"), name.text)
577 }
578}
579
580fn python_class_name(name: &Identifier, scope: &[String]) -> String {
581 if scope.is_empty() {
582 name.text.clone()
583 } else {
584 let mut parts = scope.to_vec();
585 parts.push(name.text.clone());
586 parts.join("_")
587 }
588}
589
590fn escape_python_keyword(name: &str) -> String {
591 const PYTHON_KEYWORDS: &[&str] = &[
592 "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
593 "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
594 "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
595 "try", "while", "with", "yield", "match", "case",
596 ];
597 if PYTHON_KEYWORDS.contains(&name) {
598 format!("{name}_")
599 } else {
600 name.to_string()
601 }
602}
603
604fn eval_const_int(expr: &ConstExpr) -> Option<i64> {
610 match expr {
611 ConstExpr::Literal(Literal {
612 kind: LiteralKind::Integer,
613 raw,
614 ..
615 }) => parse_integer_literal(raw),
616 ConstExpr::Unary { op, operand, .. } => {
617 let inner = eval_const_int(operand)?;
618 match op {
619 UnaryOp::Plus => Some(inner),
620 UnaryOp::Minus => Some(-inner),
621 UnaryOp::BitNot => Some(!inner),
622 }
623 }
624 _ => None,
625 }
626}
627
628fn parse_integer_literal(raw: &str) -> Option<i64> {
629 let s = raw.trim();
630 let s = s.trim_end_matches(['L', 'l', 'u', 'U']);
634 if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
635 i64::from_str_radix(hex, 16).ok()
636 } else if let Some(rest) = s.strip_prefix("-0x").or_else(|| s.strip_prefix("-0X")) {
637 i64::from_str_radix(rest, 16).ok().map(|n| -n)
638 } else if s.starts_with('0') && s.len() > 1 && !s.contains(|c: char| !c.is_ascii_digit()) {
639 i64::from_str_radix(&s[1..], 8).ok()
641 } else {
642 s.parse::<i64>().ok()
643 }
644}