use std::collections::BTreeSet;
use std::fmt::Write as _;
use zerodds_idl::ast::types::{
BinaryOp, BitmaskDecl, BitsetDecl, CaseLabel, ConstDecl, ConstExpr, ConstType, ConstrTypeDecl,
Declarator, Definition, EnumDef, ExceptDecl, FloatingType, Identifier, IntegerType, Literal,
LiteralKind, Member, ModuleDef, PrimitiveType, ScopedName, SequenceType, Specification,
StringType, StructDcl, StructDef, SwitchTypeSpec, TypeDecl, TypeSpec, TypedefDecl, UnaryOp,
UnionDcl, UnionDef,
};
use zerodds_idl::semantics::annotations::{
BuiltinAnnotation, ExtensibilityKind, lower_annotations,
};
use crate::error::{IdlPythonError, Result};
#[derive(Debug, Clone, Default)]
pub struct PythonGenOptions {
pub header_comment: Option<String>,
}
thread_local! {
static ENUM_VALUES: std::cell::RefCell<std::collections::BTreeMap<String, i64>> =
const { std::cell::RefCell::new(std::collections::BTreeMap::new()) };
static CONST_VALUES: std::cell::RefCell<std::collections::BTreeMap<String, i64>> =
const { std::cell::RefCell::new(std::collections::BTreeMap::new()) };
static TYPE_PATHS: std::cell::RefCell<Vec<Vec<String>>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn register_type_paths(defs: &[Definition], scope: &mut Vec<String>) {
for def in defs {
let name: Option<&Identifier> = match def {
Definition::Module(m) => {
scope.push(m.name.text.clone());
register_type_paths(&m.definitions, scope);
scope.pop();
None
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
Some(&s.name)
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => Some(&e.name),
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => Some(&b.name),
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => Some(&b.name),
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
Some(&u.name)
}
Definition::Type(TypeDecl::Typedef(td)) => {
for d in &td.declarators {
let mut path = scope.clone();
path.push(d.name().text.clone());
TYPE_PATHS.with(|t| t.borrow_mut().push(path));
}
None
}
Definition::Except(e) => Some(&e.name),
Definition::Const(c) => Some(&c.name),
_ => None,
};
if let Some(id) = name {
let mut path = scope.clone();
path.push(id.text.clone());
TYPE_PATHS.with(|t| t.borrow_mut().push(path));
}
}
}
fn resolve_scoped_name(name: &ScopedName, scope: &[String]) -> String {
let parts: Vec<String> = name.parts.iter().map(|p| p.text.clone()).collect();
let known: Vec<Vec<String>> = TYPE_PATHS.with(|t| t.borrow().clone());
for cut in (0..=scope.len()).rev() {
let mut candidate = scope[..cut].to_vec();
candidate.extend(parts.iter().cloned());
if known.contains(&candidate) {
return candidate.join("_");
}
}
parts.join("_")
}
fn member_is_optional(m: &Member) -> bool {
let Ok(lowered) = lower_annotations(&m.annotations) else {
return false;
};
lowered
.builtins
.iter()
.any(|b| matches!(b, BuiltinAnnotation::Optional))
}
fn member_explicit_id(m: &Member) -> Option<u32> {
let lowered = lower_annotations(&m.annotations).ok()?;
lowered.builtins.iter().find_map(|b| match b {
BuiltinAnnotation::Id(n) => Some(*n),
_ => None,
})
}
fn bitmask_bit_bound(b: &BitmaskDecl) -> u32 {
lower_annotations(&b.annotations)
.ok()
.and_then(|l| {
l.builtins.iter().find_map(|a| match a {
BuiltinAnnotation::BitBound(n) => Some(u32::from(*n)),
_ => None,
})
})
.unwrap_or(32)
}
fn enum_bit_bound(e: &EnumDef) -> u32 {
lower_annotations(&e.annotations)
.ok()
.and_then(|l| {
l.builtins.iter().find_map(|a| match a {
BuiltinAnnotation::BitBound(n) => Some(u32::from(*n)),
_ => None,
})
})
.filter(|&v| (1..=32).contains(&v))
.unwrap_or(32)
}
fn mutable_member_ids(s: &StructDef) -> Vec<u32> {
let mut ids = Vec::new();
let mut next: u32 = 0;
for m in &s.members {
let explicit = member_explicit_id(m);
for _ in &m.declarators {
let id = explicit.unwrap_or(next);
ids.push(id);
next = id.saturating_add(1);
}
}
ids
}
fn register_enum_values(defs: &[Definition]) {
for def in defs {
match def {
Definition::Module(m) => register_enum_values(&m.definitions),
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
for (idx, en) in e.enumerators.iter().enumerate() {
ENUM_VALUES.with(|m| {
m.borrow_mut().insert(en.name.text.clone(), idx as i64);
});
}
}
Definition::Const(c) => {
if let Some(v) = eval_const_int(&c.value) {
CONST_VALUES.with(|m| {
m.borrow_mut().insert(c.name.text.clone(), v);
});
}
}
_ => {}
}
}
}
pub fn generate_python_module(spec: &Specification, opts: &PythonGenOptions) -> Result<String> {
ENUM_VALUES.with(|m| m.borrow_mut().clear());
CONST_VALUES.with(|m| m.borrow_mut().clear());
register_enum_values(&spec.definitions);
TYPE_PATHS.with(|t| t.borrow_mut().clear());
{
let mut path_scope: Vec<String> = Vec::new();
register_type_paths(&spec.definitions, &mut path_scope);
}
let mut imports = ImportSet::default();
collect_imports(&spec.definitions, &mut imports)?;
let mut out = String::new();
out.push_str("# SPDX-License-Identifier: Apache-2.0\n");
if let Some(c) = &opts.header_comment {
for line in c.lines() {
out.push_str("# ");
out.push_str(line);
out.push('\n');
}
}
out.push_str("# Auto-generated by `zerodds-idl-python`. Do not edit by hand.\n");
out.push('\n');
imports.emit(&mut out);
let mut scope: Vec<String> = Vec::new();
emit_definitions(&mut out, &spec.definitions, &mut scope)?;
Ok(out)
}
#[derive(Default)]
struct ImportSet {
dataclass: bool,
int_enum: bool,
int_flag: bool,
typing_list: bool,
typing_dict: bool,
typing_type_alias: bool,
zerodds_brands: BTreeSet<&'static str>,
zerodds_idl_struct: bool,
zerodds_idl_union: bool,
}
impl ImportSet {
fn emit(&self, out: &mut String) {
if self.dataclass {
out.push_str("from dataclasses import dataclass\n");
}
if self.int_enum || self.int_flag {
out.push_str("from enum import ");
let mut parts: Vec<&str> = Vec::new();
if self.int_enum {
parts.push("IntEnum");
}
if self.int_flag {
parts.push("IntFlag");
}
out.push_str(&parts.join(", "));
out.push('\n');
}
if self.typing_list || self.typing_dict || self.typing_type_alias {
out.push_str("from typing import ");
let mut parts: Vec<&str> = Vec::new();
if self.typing_dict {
parts.push("Dict");
}
if self.typing_list {
parts.push("List");
}
if self.typing_type_alias {
parts.push("TypeAlias");
}
out.push_str(&parts.join(", "));
out.push('\n');
}
if self.zerodds_idl_struct || self.zerodds_idl_union || !self.zerodds_brands.is_empty() {
out.push_str("from zerodds.idl import ");
let mut parts: Vec<String> = Vec::new();
if self.zerodds_idl_struct {
parts.push("idl_struct".into());
}
if self.zerodds_idl_union {
parts.push("idl_union".into());
}
for brand in &self.zerodds_brands {
parts.push((*brand).to_string());
}
out.push_str(&parts.join(", "));
out.push('\n');
}
out.push('\n');
}
}
fn collect_imports(defs: &[Definition], imports: &mut ImportSet) -> Result<()> {
for d in defs {
match d {
Definition::Module(m) => collect_imports(&m.definitions, imports)?,
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
imports.dataclass = true;
imports.zerodds_idl_struct = true;
for m in &s.members {
collect_type_imports(&m.type_spec, imports)?;
collect_member_brand_imports(m, imports);
}
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(_))) => {
imports.int_enum = true;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(_))) => {
imports.int_flag = true;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(_))) => {
imports.zerodds_brands.insert("Bitset");
imports.typing_type_alias = true;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
imports.zerodds_idl_union = true;
imports.int_enum = true;
collect_switch_type_imports(&u.switch_type, imports);
for case in &u.cases {
collect_type_imports(&case.element.type_spec, imports)?;
}
}
Definition::Type(TypeDecl::Typedef(td)) => {
imports.typing_type_alias = true;
collect_type_imports(&td.type_spec, imports)?;
if td
.declarators
.iter()
.any(|d| matches!(d, Declarator::Array(_)))
{
imports.zerodds_brands.insert("Array");
}
}
Definition::Except(e) => {
imports.dataclass = true;
imports.zerodds_idl_struct = true;
for m in &e.members {
collect_type_imports(&m.type_spec, imports)?;
collect_member_brand_imports(m, imports);
}
}
_ => {
}
}
}
Ok(())
}
fn collect_member_brand_imports(m: &Member, imports: &mut ImportSet) {
if m.declarators
.iter()
.any(|d| matches!(d, Declarator::Array(_)))
{
imports.zerodds_brands.insert("Array");
}
if member_is_optional(m) {
imports.zerodds_brands.insert("Optional");
}
}
fn collect_type_imports(ts: &TypeSpec, imports: &mut ImportSet) -> Result<()> {
match ts {
TypeSpec::Primitive(p) => {
imports.zerodds_brands.insert(brand_for_primitive(*p));
}
TypeSpec::String(s) => {
if s.bound.as_ref().and_then(eval_const_int).is_some() {
imports.zerodds_brands.insert(if s.wide {
"BoundedWString"
} else {
"BoundedString"
});
} else {
imports.zerodds_brands.insert(brand_for_string(s));
}
}
TypeSpec::Sequence(seq) => {
if seq.bound.as_ref().and_then(eval_const_int).is_some() {
imports.zerodds_brands.insert("Sequence");
} else {
imports.typing_list = true;
}
collect_type_imports(&seq.elem, imports)?;
}
TypeSpec::Scoped(_) => {
}
TypeSpec::Map(m) => {
if m.bound.as_ref().and_then(eval_const_int).is_some() {
imports.zerodds_brands.insert("Map");
} else {
imports.typing_dict = true;
}
collect_type_imports(&m.key, imports)?;
collect_type_imports(&m.value, imports)?;
}
TypeSpec::Fixed(_) => {
imports.zerodds_brands.insert("Fixed");
}
TypeSpec::Any => {
return Err(IdlPythonError::Unsupported(format!(
"type spec not yet supported in python codegen: {ts:?}"
)));
}
}
Ok(())
}
fn collect_switch_type_imports(switch: &SwitchTypeSpec, imports: &mut ImportSet) {
match switch {
SwitchTypeSpec::Integer(i) => {
imports.zerodds_brands.insert(brand_for_integer(*i));
}
SwitchTypeSpec::Boolean => {
}
SwitchTypeSpec::Octet => {
imports.zerodds_brands.insert("Octet");
}
SwitchTypeSpec::Char => {
imports.zerodds_brands.insert("Char");
}
SwitchTypeSpec::Scoped(_) => {
}
}
}
fn emit_definitions(out: &mut String, defs: &[Definition], scope: &mut Vec<String>) -> Result<()> {
for d in defs {
match d {
Definition::Module(m) => emit_module(out, m, scope)?,
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Def(s)))) => {
emit_struct(out, s, scope)?;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Struct(StructDcl::Forward(_)))) => {
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Enum(e))) => {
emit_enum(out, e, scope)?;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitmask(b))) => {
emit_bitmask(out, b, scope)?;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Bitset(b))) => {
emit_bitset(out, b, scope)?;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Def(u)))) => {
emit_union(out, u, scope)?;
}
Definition::Type(TypeDecl::Constr(ConstrTypeDecl::Union(UnionDcl::Forward(_)))) => {
}
Definition::Type(TypeDecl::Typedef(td)) => {
emit_typedef(out, td, scope)?;
}
Definition::Except(e) => {
emit_exception(out, e, scope)?;
}
Definition::Const(c) => {
emit_const(out, c, scope)?;
}
_ => {
return Err(IdlPythonError::Unsupported(format!(
"definition variant not yet supported: {d:?}"
)));
}
}
}
Ok(())
}
fn emit_module(out: &mut String, m: &ModuleDef, scope: &mut Vec<String>) -> Result<()> {
scope.push(m.name.text.clone());
emit_definitions(out, &m.definitions, scope)?;
scope.pop();
Ok(())
}
fn struct_extensibility_kwarg(s: &StructDef) -> Option<&'static str> {
let lowered = lower_annotations(&s.annotations).ok()?;
match lowered.extensibility() {
Some(ExtensibilityKind::Final) => None, Some(ExtensibilityKind::Appendable) => Some("appendable"),
Some(ExtensibilityKind::Mutable) => Some("mutable"),
None => Some("appendable"), }
}
fn union_extensibility_kwarg(u: &UnionDef) -> Option<&'static str> {
let lowered = lower_annotations(&u.annotations).ok()?;
match lowered.extensibility() {
Some(ExtensibilityKind::Final) => None,
Some(ExtensibilityKind::Appendable) => Some("appendable"),
Some(ExtensibilityKind::Mutable) => Some("mutable"),
None => Some("appendable"), }
}
fn emit_struct(out: &mut String, s: &StructDef, scope: &[String]) -> Result<()> {
let typename = qualified_typename(&s.name, scope);
let class_name = python_class_name(&s.name, scope);
match struct_extensibility_kwarg(s) {
Some("mutable") => {
let ids = mutable_member_ids(s);
let id_list = ids
.iter()
.map(u32::to_string)
.collect::<Vec<_>>()
.join(", ");
writeln!(
out,
"@idl_struct(typename=\"{typename}\", extensibility=\"mutable\", \
member_ids=[{id_list}])"
)
.ok();
}
Some(ext) => {
writeln!(
out,
"@idl_struct(typename=\"{typename}\", extensibility=\"{ext}\")"
)
.ok();
}
None => {
writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
}
}
writeln!(out, "@dataclass").ok();
if let Some(base) = &s.base {
let base_class = resolve_scoped_name(base, scope);
writeln!(out, "class {class_name}({base_class}):").ok();
} else {
writeln!(out, "class {class_name}:").ok();
}
if s.members.is_empty() && s.base.is_none() {
writeln!(out, " pass").ok();
} else if s.members.is_empty() {
writeln!(out, " pass").ok();
} else {
for m in &s.members {
emit_members_of(out, m, scope)?;
}
}
out.push('\n');
Ok(())
}
fn emit_exception(out: &mut String, e: &ExceptDecl, scope: &[String]) -> Result<()> {
let typename = qualified_typename(&e.name, scope);
let class_name = python_class_name(&e.name, scope);
writeln!(out, "@idl_struct(typename=\"{typename}\")").ok();
writeln!(out, "@dataclass").ok();
writeln!(out, "class {class_name}(Exception):").ok();
if e.members.is_empty() {
writeln!(out, " pass").ok();
} else {
for m in &e.members {
emit_members_of(out, m, scope)?;
}
}
out.push('\n');
Ok(())
}
fn emit_members_of(out: &mut String, m: &Member, scope: &[String]) -> Result<()> {
let py_type = python_type_for(&m.type_spec, scope)?;
let optional = member_is_optional(m);
for d in &m.declarators {
let field_name = escape_python_keyword(d.name().text.as_str());
let mut t = py_type.clone();
if let Declarator::Array(arr) = d {
for size in arr.sizes.iter().rev() {
let n = eval_const_int(size).ok_or_else(|| {
IdlPythonError::Unsupported(format!(
"array bound is not a literal integer: {size:?}"
))
})?;
t = format!("Array[{t}, {n}]");
}
}
if optional {
t = format!("Optional[{t}]");
}
writeln!(out, " {field_name}: {t}").ok();
}
Ok(())
}
fn emit_enum(out: &mut String, e: &EnumDef, scope: &[String]) -> Result<()> {
let class_name = python_class_name(&e.name, scope);
writeln!(out, "class {class_name}(IntEnum):").ok();
if e.enumerators.is_empty() {
writeln!(out, " pass").ok();
} else {
for (idx, en) in e.enumerators.iter().enumerate() {
writeln!(out, " {} = {idx}", en.name.text).ok();
}
}
let bound = enum_bit_bound(e);
if bound != 32 {
writeln!(out, "{class_name}._idl_bit_bound = {bound}").ok();
}
out.push('\n');
Ok(())
}
fn emit_bitmask(out: &mut String, b: &BitmaskDecl, scope: &[String]) -> Result<()> {
let class_name = python_class_name(&b.name, scope);
writeln!(out, "class {class_name}(IntFlag):").ok();
if b.values.is_empty() {
writeln!(out, " pass").ok();
} else {
for (idx, v) in b.values.iter().enumerate() {
writeln!(out, " {} = 1 << {idx}", v.name.text).ok();
}
}
writeln!(
out,
"{class_name}._idl_bit_bound = {}",
bitmask_bit_bound(b)
)
.ok();
out.push('\n');
Ok(())
}
fn emit_bitset(out: &mut String, b: &BitsetDecl, scope: &[String]) -> Result<()> {
let class_name = python_class_name(&b.name, scope);
let total_bits: i64 = b
.bitfields
.iter()
.map(|bf| eval_const_int(&bf.spec.width).unwrap_or(0))
.sum();
writeln!(out, "{class_name}: TypeAlias = Bitset[{total_bits}]").ok();
writeln!(out).ok();
writeln!(out, "class {class_name}_Bits:").ok();
if b.bitfields.is_empty() {
writeln!(out, " pass").ok();
} else {
let mut shift: u64 = 0;
for bf in &b.bitfields {
let width = eval_const_int(&bf.spec.width).ok_or_else(|| {
IdlPythonError::Unsupported(format!(
"bitset width is not a literal integer: {:?}",
bf.spec.width
))
})?;
if width <= 0 {
return Err(IdlPythonError::Unsupported(format!(
"bitset width must be > 0, got {width}"
)));
}
let width = width as u64;
let mask = if width >= 64 {
u64::MAX
} else {
(1u64 << width) - 1
};
if let Some(name) = &bf.name {
let up = name.text.to_uppercase();
writeln!(out, " {up}_SHIFT = {shift}").ok();
writeln!(out, " {up}_WIDTH = {width}").ok();
writeln!(out, " {up}_MASK = 0x{mask:x}").ok();
}
shift += width;
}
}
out.push('\n');
Ok(())
}
fn emit_union(out: &mut String, u: &UnionDef, scope: &[String]) -> Result<()> {
let typename = qualified_typename(&u.name, scope);
let class_name = python_class_name(&u.name, scope);
let discriminator_repr = switch_type_python_repr(&u.switch_type, scope);
let mut case_entries: Vec<(i64, String, String)> = Vec::new();
let mut default_entry: Option<(String, String)> = None;
for case in &u.cases {
let field_name = escape_python_keyword(case.element.declarator.name().text.as_str());
let py_type = python_type_for(&case.element.type_spec, scope)?;
for label in &case.labels {
match label {
CaseLabel::Value(expr) => {
let v = eval_const_int(expr).ok_or_else(|| {
IdlPythonError::Unsupported(format!(
"union case label is not a literal integer (scoped/enum-ref \
references will be supported in a follow-up): {expr:?}"
))
})?;
case_entries.push((v, field_name.clone(), py_type.clone()));
}
CaseLabel::Default => {
default_entry = Some((field_name.clone(), py_type.clone()));
}
}
}
}
writeln!(out, "{class_name} = idl_union(").ok();
writeln!(out, " typename=\"{typename}\",").ok();
writeln!(out, " discriminator={discriminator_repr},").ok();
writeln!(out, " cases={{").ok();
for (label_value, field, py_type) in &case_entries {
writeln!(out, " {label_value}: (\"{field}\", {py_type}),").ok();
}
writeln!(out, " }},").ok();
if let Some((field, py_type)) = &default_entry {
writeln!(out, " default=(\"{field}\", {py_type}),").ok();
} else {
writeln!(out, " default=None,").ok();
}
if let Some(ext) = union_extensibility_kwarg(u) {
writeln!(out, " extensibility=\"{ext}\",").ok();
}
writeln!(out, ")").ok();
out.push('\n');
Ok(())
}
fn emit_typedef(out: &mut String, td: &TypedefDecl, scope: &[String]) -> Result<()> {
let py_type = python_type_for(&td.type_spec, scope)?;
for d in &td.declarators {
let alias_name = python_class_name(d.name(), scope);
match d {
Declarator::Simple(_) => {
writeln!(out, "{alias_name}: TypeAlias = {py_type}").ok();
}
Declarator::Array(arr) => {
let mut t = py_type.clone();
for size in arr.sizes.iter().rev() {
let n = eval_const_int(size).ok_or_else(|| {
IdlPythonError::Unsupported(format!(
"array bound is not a literal integer: {size:?}"
))
})?;
t = format!("Array[{t}, {n}]");
}
writeln!(out, "{alias_name}: TypeAlias = {t}").ok();
}
}
}
out.push('\n');
Ok(())
}
fn emit_const(out: &mut String, c: &ConstDecl, scope: &[String]) -> Result<()> {
let name = python_class_name(&c.name, scope);
let value = render_const_value(&c.value, &c.type_, scope)?;
writeln!(out, "{name} = {value}").ok();
out.push('\n');
Ok(())
}
fn render_const_value(expr: &ConstExpr, ty: &ConstType, scope: &[String]) -> Result<String> {
if let ConstExpr::Literal(lit) = expr {
match lit.kind {
LiteralKind::Boolean => {
let v = lit.raw.trim().eq_ignore_ascii_case("true");
return Ok(if v { "True".into() } else { "False".into() });
}
LiteralKind::Floating | LiteralKind::Fixed => {
return Ok(lit
.raw
.trim()
.trim_end_matches(['f', 'F', 'd', 'D'])
.to_string());
}
LiteralKind::String | LiteralKind::WideString => {
return Ok(lit.raw.clone());
}
LiteralKind::Char | LiteralKind::WideChar => {
return Ok(lit.raw.clone());
}
LiteralKind::Integer => {}
}
}
if let Some(n) = eval_const_int(expr) {
return Ok(n.to_string());
}
if let ConstExpr::Scoped(s) = expr {
return Ok(resolve_scoped_name(s, scope));
}
Err(IdlPythonError::Unsupported(format!(
"const value not representable in python codegen (type {ty:?}): {expr:?}"
)))
}
fn python_type_for(ts: &TypeSpec, scope: &[String]) -> Result<String> {
match ts {
TypeSpec::Primitive(p) => Ok(brand_for_primitive(*p).to_string()),
TypeSpec::String(s) => {
if let Some(n) = s.bound.as_ref().and_then(eval_const_int) {
let brand = if s.wide {
"BoundedWString"
} else {
"BoundedString"
};
Ok(format!("{brand}[{n}]"))
} else {
Ok(brand_for_string(s).to_string())
}
}
TypeSpec::Sequence(seq) => {
let inner = python_type_for_seq_elem(seq, scope)?;
if let Some(n) = seq.bound.as_ref().and_then(eval_const_int) {
Ok(format!("Sequence[{inner}, {n}]"))
} else {
Ok(format!("List[{inner}]"))
}
}
TypeSpec::Scoped(name) => Ok(resolve_scoped_name(name, scope)),
TypeSpec::Map(m) => {
let k = python_type_for(&m.key, scope)?;
let v = python_type_for(&m.value, scope)?;
if let Some(n) = m.bound.as_ref().and_then(eval_const_int) {
Ok(format!("Map[{k}, {v}, {n}]"))
} else {
Ok(format!("Dict[{k}, {v}]"))
}
}
TypeSpec::Fixed(f) => {
let p = eval_const_int(&f.digits).unwrap_or(0);
let s = eval_const_int(&f.scale).unwrap_or(0);
Ok(format!("Fixed[{p}, {s}]"))
}
TypeSpec::Any => Err(IdlPythonError::Unsupported(format!(
"type spec not yet supported in python codegen: {ts:?}"
))),
}
}
fn python_type_for_seq_elem(seq: &SequenceType, scope: &[String]) -> Result<String> {
python_type_for(&seq.elem, scope)
}
fn brand_for_primitive(p: PrimitiveType) -> &'static str {
match p {
PrimitiveType::Boolean => "Bool",
PrimitiveType::Octet => "Octet",
PrimitiveType::Char => "Char",
PrimitiveType::WideChar => "WChar",
PrimitiveType::Integer(i) => brand_for_integer(i),
PrimitiveType::Floating(f) => match f {
FloatingType::Float => "Float32",
FloatingType::Double => "Float64",
FloatingType::LongDouble => "LongDouble",
},
}
}
fn brand_for_integer(i: IntegerType) -> &'static str {
match i {
IntegerType::Short | IntegerType::Int16 => "Int16",
IntegerType::UShort | IntegerType::UInt16 => "UInt16",
IntegerType::Long | IntegerType::Int32 => "Int32",
IntegerType::ULong | IntegerType::UInt32 => "UInt32",
IntegerType::LongLong | IntegerType::Int64 => "Int64",
IntegerType::ULongLong | IntegerType::UInt64 => "UInt64",
IntegerType::Int8 => "Int8",
IntegerType::UInt8 => "UInt8",
}
}
fn brand_for_string(s: &StringType) -> &'static str {
if s.wide { "WString" } else { "String" }
}
fn switch_type_python_repr(switch: &SwitchTypeSpec, scope: &[String]) -> String {
match switch {
SwitchTypeSpec::Integer(i) => brand_for_integer(*i).to_string(),
SwitchTypeSpec::Boolean => "bool".to_string(),
SwitchTypeSpec::Octet => "Octet".to_string(),
SwitchTypeSpec::Char => "Char".to_string(),
SwitchTypeSpec::Scoped(s) => resolve_scoped_name(s, scope),
}
}
fn qualified_typename(name: &Identifier, scope: &[String]) -> String {
if scope.is_empty() {
name.text.clone()
} else {
format!("{}::{}", scope.join("::"), name.text)
}
}
fn python_class_name(name: &Identifier, scope: &[String]) -> String {
if scope.is_empty() {
name.text.clone()
} else {
let mut parts = scope.to_vec();
parts.push(name.text.clone());
parts.join("_")
}
}
fn escape_python_keyword(name: &str) -> String {
const PYTHON_KEYWORDS: &[&str] = &[
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
"try", "while", "with", "yield", "match", "case",
];
if PYTHON_KEYWORDS.contains(&name) {
format!("{name}_")
} else {
name.to_string()
}
}
fn eval_const_int(expr: &ConstExpr) -> Option<i64> {
match expr {
ConstExpr::Literal(Literal {
kind: LiteralKind::Integer,
raw,
..
}) => parse_integer_literal(raw),
ConstExpr::Unary { op, operand, .. } => {
let inner = eval_const_int(operand)?;
match op {
UnaryOp::Plus => Some(inner),
UnaryOp::Minus => Some(-inner),
UnaryOp::BitNot => Some(!inner),
}
}
ConstExpr::Scoped(s) => {
let name = s.parts.last()?.text.clone();
ENUM_VALUES
.with(|m| m.borrow().get(&name).copied())
.or_else(|| CONST_VALUES.with(|m| m.borrow().get(&name).copied()))
}
ConstExpr::Binary { op, lhs, rhs, .. } => {
let l = eval_const_int(lhs)?;
let r = eval_const_int(rhs)?;
match op {
BinaryOp::Or => Some(l | r),
BinaryOp::Xor => Some(l ^ r),
BinaryOp::And => Some(l & r),
BinaryOp::Shl => Some(l.checked_shl(u32::try_from(r).ok()?)?),
BinaryOp::Shr => Some(l.checked_shr(u32::try_from(r).ok()?)?),
BinaryOp::Add => l.checked_add(r),
BinaryOp::Sub => l.checked_sub(r),
BinaryOp::Mul => l.checked_mul(r),
BinaryOp::Div => l.checked_div(r),
BinaryOp::Mod => l.checked_rem(r),
}
}
_ => None,
}
}
fn parse_integer_literal(raw: &str) -> Option<i64> {
let s = raw.trim();
let s = s.trim_end_matches(['L', 'l', 'u', 'U']);
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
i64::from_str_radix(hex, 16).ok()
} else if let Some(rest) = s.strip_prefix("-0x").or_else(|| s.strip_prefix("-0X")) {
i64::from_str_radix(rest, 16).ok().map(|n| -n)
} else if s.starts_with('0') && s.len() > 1 && !s.contains(|c: char| !c.is_ascii_digit()) {
i64::from_str_radix(&s[1..], 8).ok()
} else {
s.parse::<i64>().ok()
}
}