use crate::adapter::c::ordered_types;
use crate::adapter::constant;
use crate::adapter::cst::{MaterialiseError, no_spelling_collision};
use crate::apply::Applied;
use crate::verbatim;
use seesaw_tgg::ident::GhostId;
use zerodds_ast::{Ast, Bound, CharWidth, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::python::{
Annotated, Arg, Assign, Class, Decorator, Expr, Import, Keyword, Module, Statement, Type,
escape_name,
};
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
const PRELUDE_NAMES: &[&str] = &[
"_IdlInt",
"_IdlFloat",
"_IdlText",
"_IdlFixed",
"_IdlSequence",
"_IdlEnum",
"_Int8",
"_UInt8",
"_Int16",
"_UInt16",
"_Int32",
"_UInt32",
"_Int64",
"_UInt64",
"_Float32",
"_Float64",
"_Float128",
"_Char",
"_WChar",
];
pub fn python_module(ast: &Ast, applied: &Applied) -> Result<Module, MaterialiseError> {
let mut items: Vec<Pending> = Vec::new();
for id in ordered_types(ast) {
let start = items.len();
declare(ast, applied, id, &mut items)?;
let Ok(node) = ast.type_(id) else {
continue;
};
let module = node.module;
let name = escape_name(&node.name);
verbatim::weave_with(
verbatim::declaration_slots(ast, id, verbatim::aliases::PYTHON),
start,
&mut items,
|text| Pending {
module,
statement: Statement::Verbatim(text),
},
|declared, begin, end| {
for pending in declared {
if verbatim::Carrier::take_into_body(&mut pending.statement, &name, begin, end)
{
return;
}
}
},
);
}
for (id, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "PythonConst").is_none() {
continue;
}
let _ = id;
let value = ast
.eval_const(node.expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "constant's value".to_string(),
at: node.name.clone(),
})?;
let (type_, kind) = match node.type_.resolved() {
Some(target) => (
python_member_type(ast, target, &node.name)?,
scalar_kind(ast, target),
),
None => (alias(INT32), Some(PrimitiveKind::Int32)),
};
let value = constant_expr(kind, &value, &node.name)?;
items.push(Pending {
module: node.module,
statement: Statement::Annotated(Annotated {
doc: renamed_note(&node.name),
name: Ident::new(&escape_name(&node.name)),
type_: Type::Subscript {
base: Path::new(&["typing", "Final"]),
args: vec![type_],
},
value: Some(value),
}),
});
}
for (id, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "PythonError").is_none() {
continue;
}
let _ = id;
let mut body = Vec::new();
for member in &node.members {
let Ok(member_node) = ast.member(*member) else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "exception member type".to_string(),
at: member_node.name.clone(),
});
};
body.push(field(
&member_node.name,
python_member_type(ast, target, &member_node.name)?,
member_node.facets.is_optional(),
));
}
items.push(Pending {
module: node.module,
statement: dataclass(
&node.name,
body,
Doc::lines(&[
"An exception, as the structure its members make.",
"",
"A dataclass and not a subclass of Exception: raising is a",
"run-time decision the RPC arc makes, and a BaseException",
"subclass carries __traceback__ and args, which are two more",
"attributes than the wire form has.",
]),
),
});
}
let mut root = Scope::default();
for item in items {
insert(ast, &mut root, item)?;
}
let body = scope_body(root, 0)?;
check_scope(&body, 0)?;
let mut statements = prelude();
statements.extend(body);
let statements = verbatim::weave_file(ast, verbatim::aliases::PYTHON, statements);
Ok(Module {
doc: Doc::lines(&[
"The Python shape of one IDL file.",
"",
"Generated by zerodds-lang-rules. Do not edit.",
"",
"Every IDL fact Python cannot enforce in a type is written into",
"typing.Annotated metadata: the width of an integer, the bound of a",
"string or a sequence, the digit count of a fixed. Read it back with",
"typing.get_type_hints(cls, include_extras=True).",
"",
"Requires Python 3.10 or newer, for dataclasses.dataclass(kw_only=True).",
]),
future: vec![Ident::new("annotations")],
imports: vec![
Import::Module {
path: Path::single("dataclasses"),
alias: None,
},
Import::Module {
path: Path::single("enum"),
alias: None,
},
Import::Module {
path: Path::single("typing"),
alias: None,
},
],
body: statements,
})
}
struct Pending {
module: Id<ModuleNode>,
statement: Statement,
}
impl verbatim::Carrier for Statement {
fn verbatim(text: String) -> Self {
Self::Verbatim(text)
}
fn take_into_body(
&mut self,
name: &str,
begin: &mut Vec<String>,
end: &mut Vec<String>,
) -> bool {
let Self::Class(class) = self else {
return false;
};
if class.name.as_str() != name {
return false;
}
let mut body: Vec<Statement> = begin.drain(..).map(Statement::Verbatim).collect();
body.append(&mut class.body);
body.extend(end.drain(..).map(Statement::Verbatim));
class.body = body;
true
}
}
#[derive(Default)]
struct Scope {
body: Vec<Statement>,
children: Vec<(Id<ModuleNode>, String, Scope)>,
}
fn insert(ast: &Ast, root: &mut Scope, item: Pending) -> Result<(), MaterialiseError> {
let chain = module_chain(ast, item.module)?;
let mut scope = root;
for (id, name) in chain {
let at = match scope.children.iter().position(|(seen, _, _)| *seen == id) {
Some(at) => at,
None => {
scope.children.push((id, name, Scope::default()));
scope.children.len() - 1
}
};
scope = &mut scope
.children
.get_mut(at)
.ok_or_else(|| MaterialiseError::NoModule {
at: module_handle(item.module),
})?
.2;
}
scope.body.push(item.statement);
Ok(())
}
fn scope_body(scope: Scope, depth: usize) -> Result<Vec<Statement>, MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut out = scope.body;
for (_, name, child) in scope.children {
let body = scope_body(child, depth + 1)?;
out.push(Statement::Class(Class {
doc: Doc::line("An IDL module, as the class its declarations nest in."),
decorators: Vec::new(),
name: Ident::new(&name),
bases: Vec::new(),
body: if body.is_empty() {
vec![Statement::Pass]
} else {
body
},
}));
}
Ok(out)
}
fn module_chain(
ast: &Ast,
module: Id<ModuleNode>,
) -> Result<Vec<(Id<ModuleNode>, String)>, MaterialiseError> {
let mut out = Vec::new();
let mut current = Some(module);
let mut depth = 0;
while let Some(id) = current {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!(
"{} — the parent chain runs past {MAX_MODULE_DEPTH}",
module_handle(module)
),
});
}
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if node.parent.is_none() {
break;
}
out.push((id, escape_name(&node.name)));
current = node.parent;
depth += 1;
}
out.reverse();
Ok(out)
}
fn module_handle(module: Id<ModuleNode>) -> String {
format!("#{}@{}", module.slot(), module.generation())
}
fn check_scope(body: &[Statement], depth: usize) -> Result<(), MaterialiseError> {
if depth > MAX_MODULE_DEPTH {
return Err(MaterialiseError::NoModule {
at: format!("the nesting runs past {MAX_MODULE_DEPTH}"),
});
}
let mut seen: Vec<&str> = Vec::new();
for statement in body {
let name = match statement {
Statement::Class(class) => class.name.as_str(),
Statement::Annotated(binding) => binding.name.as_str(),
Statement::Assign(binding) => binding.name.as_str(),
Statement::Function(function) => function.name.as_str(),
Statement::Set { .. }
| Statement::Expr(_)
| Statement::If { .. }
| Statement::For { .. }
| Statement::While { .. }
| Statement::Break
| Statement::Return(_)
| Statement::Comment(_)
| Statement::Pass
| Statement::Verbatim(_) => continue,
};
if seen.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a second declaration binding the name `{name}` in one scope, \
which would silently replace the first"
),
language: "Python",
at: name.to_string(),
});
}
if PRELUDE_NAMES.contains(&name) {
return Err(MaterialiseError::NoLanguageForm {
construct: format!(
"a declaration named `{name}`, which is one of the names the \
generated preamble binds"
),
language: "Python",
at: name.to_string(),
});
}
seen.push(name);
if let Statement::Class(class) = statement {
check_scope(&class.body, depth + 1)?;
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
items: &mut Vec<Pending>,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Map { .. } => {}
TypeKind::Array { dims, .. } => {
for dim in dims {
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: named(&node.name, node.node_id),
})?;
}
}
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "PythonAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: node.name.clone(),
});
};
let spelling =
zerodds_lang_render::python::type_(&python_member_type(ast, target, &node.name)?);
items.push(Pending {
module: node.module,
statement: Statement::Comment(Doc::line(&format!(
"typedef {spelling} {}; -- not a Python alias, which is evaluated \
eagerly and could not be ordered against the classes it names, so \
every use takes the spelling above.",
node.name
))),
});
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "PythonEnum").is_none() {
return Ok(());
}
let mut body = Vec::new();
for enumerator in enumerators {
let Ok(enum_node) = ast.enumerator(*enumerator) else {
return Err(MaterialiseError::Unevaluated {
construct: "enumerator the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, enum_node.node_id, "PythonEnumerator").is_none() {
continue;
}
let value = ast.effective_enumerator_value(*enumerator).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "enumerator value".to_string(),
at: enum_node.name.clone(),
}
})?;
let value = in_range(
value,
i128::from(i32::MIN),
i128::from(u32::MAX),
"32-bit enumerated literal",
&enum_node.name,
)?;
body.push(Statement::Assign(Assign {
doc: renamed_note(&enum_node.name),
name: Ident::new(&escape_name(&enum_node.name)),
value: Expr::int(value),
}));
}
items.push(Pending {
module: node.module,
statement: enum_class(
&node.name,
"IntEnum",
body,
Doc::lines(&[
"An enumeration. An enum.IntEnum member is an int, so the names",
"and the values are one declaration rather than two.",
"",
"A member of this type is nonetheless annotated int and not this",
"class: an @appendable writer may send a value the reader's IDL",
"does not list, and Colour(7) raises ValueError.",
]),
),
});
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "PythonBitmask").is_none() {
return Ok(());
}
let mut body = Vec::new();
for bit in bits {
let Ok(bit_node) = ast.bit_value(*bit) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitmask bit the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, bit_node.node_id, "PythonBitConst").is_none() {
continue;
}
let position = ast.effective_bit_position(*bit).ok_or_else(|| {
MaterialiseError::Unevaluated {
construct: "bit position".to_string(),
at: bit_node.name.clone(),
}
})?;
body.push(Statement::Assign(Assign {
doc: renamed_note(&bit_node.name),
name: Ident::new(&escape_name(&bit_node.name)),
value: Expr::Binary {
op: "<<",
lhs: Box::new(Expr::int(1)),
rhs: Box::new(Expr::int(i128::from(position))),
},
}));
}
items.push(Pending {
module: node.module,
statement: enum_class(
&node.name,
"IntFlag",
body,
Doc::lines(&[
"A bitmask. An enum.IntFlag member is an int and combines with |,",
"which is what a bitmask is for.",
"",
"A member of this type is annotated int: a writer may set a bit",
"this declaration does not name.",
]),
),
});
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "PythonBitset").is_none() {
return Ok(());
}
let mut body = Vec::new();
for bitfield in &ast.all_bitfields_of(id) {
let Ok(field_node) = ast.bitfield(*bitfield) else {
return Err(MaterialiseError::Unevaluated {
construct: "bitset field the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(field_name) = field_node.name.as_ref() else {
continue;
};
body.push(field(field_name, bit_width(field_node.bits), false));
}
items.push(Pending {
module: node.module,
statement: dataclass(
&node.name,
body,
Doc::lines(&[
"A bitset. Each field states its own bit width in the metadata;",
"Python has no fixed-width integer, so nothing is rounded up to",
"a holder the way the C and Java columns have to.",
"",
"The packing into the anchor is the codec's.",
]),
),
});
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"PythonValue"
} else {
"PythonStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
let mut body = Vec::new();
let mut spelled = Vec::new();
if let Some(base) = super::c::struct_base(ast, id) {
spelled.push((escape_name("parent"), "parent".to_string()));
body.push(field(
"parent",
python_member_type(ast, base, "parent")?,
false,
));
}
for member in ast.members_of(id).iter().copied() {
let Ok(member_node) = ast.member(member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
spelled.push((escape_name(&member_node.name), member_node.name.clone()));
body.push(field(
&member_node.name,
python_member_type(ast, target, &member_node.name)?,
member_node.facets.is_optional(),
));
}
no_spelling_collision(
spelled
.iter()
.map(|(field, declared)| (field.as_str(), declared.as_str())),
"Python",
&node.name,
)?;
items.push(Pending {
module: node.module,
statement: dataclass(&node.name, body, Doc::none()),
});
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "PythonUnion").is_none() {
return Ok(());
}
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: node.name.clone(),
});
};
let mut body = vec![Statement::Annotated(Annotated {
doc: Doc::line("The discriminator. Which case field is live is what this says."),
name: Ident::new("_d"),
type_: python_member_type(ast, disc, &node.name)?,
value: None,
})];
for case in ast.cases_of(id) {
let Ok(case_node) = ast.case(*case) else {
return Err(MaterialiseError::Unevaluated {
construct: "union case the model listed".to_string(),
at: node.name.clone(),
});
};
if ghost_created_for(applied, case_node.node_id, "PythonCase").is_none() {
continue;
}
let Ok(member) = ast.member(case_node.member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member behind a union case the model listed".to_string(),
at: node.name.clone(),
});
};
let Some(target) = member.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union case type".to_string(),
at: member.name.clone(),
});
};
body.push(field(
&member.name,
python_member_type(ast, target, &member.name)?,
true,
));
}
items.push(Pending {
module: node.module,
statement: dataclass(
&node.name,
body,
Doc::lines(&[
"A union: the discriminator, and one field per case.",
"",
"Every case field is Optional and defaults to None, because at",
"most one of them is live. Which one is what _d says; Python has",
"no way to state that in a type.",
]),
),
});
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "PythonNative").is_none() {
return Ok(());
}
items.push(Pending {
module: node.module,
statement: Statement::Comment(Doc::line(&format!(
"native {}; -- every use takes {}.{}",
node.name,
zerodds_lang_render::python::type_(&native_type()),
native_note(ast, id),
))),
});
}
TypeKind::ValueBox { .. } => {}
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => {}
}
Ok(())
}
pub fn python_member_type(
ast: &Ast,
target: Id<TypeNode>,
at: &str,
) -> Result<Type, MaterialiseError> {
type_at(ast, target, at, 0)
}
#[allow(clippy::too_many_lines)]
fn type_at(ast: &Ast, id: Id<TypeNode>, at: &str, depth: usize) -> Result<Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose Python spelling nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: at.to_string(),
})?;
let refused = match &node.kind {
TypeKind::Any => Some("any"),
TypeKind::ValueBase => Some("ValueBase"),
TypeKind::ObjectRef { .. } => Some("Object"),
TypeKind::AnnotationType { .. } => Some("annotation type"),
_ => None,
};
if let Some(construct) = refused {
return Err(MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: "Python",
at: at.to_string(),
});
}
Ok(match &node.kind {
TypeKind::Primitive(kind) => alias(primitive_alias(*kind)),
TypeKind::String { width, bound } => annotated(
Type::name("str"),
call(
"_IdlText",
vec![
kw("bits", Expr::int(char_bits(*width))),
kw("bound", Expr::int(bound_of(ast, *bound, at)?)),
],
),
),
TypeKind::Fixed { digits, scale } => annotated(
Type::name("str"),
call(
"_IdlFixed",
vec![
kw("digits", Expr::int(i128::from(*digits))),
kw("scale", Expr::int(i128::from(*scale))),
],
),
),
TypeKind::Sequence { element, bound } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "sequence element".to_string(),
at: at.to_string(),
});
};
let list = Type::subscript(
Path::single("list"),
vec![type_at(ast, element, at, depth + 1)?],
);
match bound_of(ast, *bound, at)? {
0 => list,
length => annotated(
list,
call("_IdlSequence", vec![kw("bound", Expr::int(length))]),
),
}
}
TypeKind::Array { element, dims } => {
let Some(element) = element.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "array element".to_string(),
at: at.to_string(),
});
};
let mut out = type_at(ast, element, at, depth + 1)?;
for dim in dims.iter().rev() {
let length =
ast.eval_integral(*dim)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "array length".to_string(),
at: at.to_string(),
})?;
let length =
usize::try_from(length).map_err(|_| MaterialiseError::NoLanguageForm {
construct: format!("an array of {length} elements"),
language: "Python",
at: at.to_string(),
})?;
out = Type::subscript(Path::single("tuple"), vec![out; length]);
}
out
}
TypeKind::Map { key, value, bound } => {
let (Some(key), Some(value)) = (key.resolved(), value.resolved()) else {
return Err(MaterialiseError::Unevaluated {
construct: "map key or value".to_string(),
at: at.to_string(),
});
};
let entry = Type::subscript(
Path::single("tuple"),
vec![
type_at(ast, key, at, depth + 1)?,
type_at(ast, value, at, depth + 1)?,
],
);
let list = Type::subscript(Path::single("list"), vec![entry]);
match bound_of(ast, *bound, at)? {
0 => list,
length => annotated(
list,
call("_IdlSequence", vec![kw("bound", Expr::int(length))]),
),
}
}
TypeKind::Alias { aliased } => {
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: at.to_string(),
});
};
type_at(ast, target, at, depth + 1)?
}
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => annotated(
Type::name("int"),
call(
"_IdlEnum",
vec![
kw("declared", Expr::Path(qualified(ast, id)?)),
kw("bits", Expr::int(i128::from(ast.effective_bit_bound(id)))),
],
),
),
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. } => Type::Name(qualified(ast, id)?),
TypeKind::Native => native_type(),
TypeKind::ValueBox { boxed: inner } => {
let Some(target) = inner.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "value box target".to_string(),
at: at.to_string(),
});
};
optional(type_at(ast, target, at, depth + 1)?)
}
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => {
return Err(MaterialiseError::NoLanguageForm {
construct: node.kind.as_str().to_string(),
language: "Python",
at: at.to_string(),
});
}
})
}
#[must_use]
pub const fn primitive_alias(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "bool",
PrimitiveKind::Char => CHAR,
PrimitiveKind::WChar => WCHAR,
PrimitiveKind::Int8 => "_Int8",
PrimitiveKind::Octet | PrimitiveKind::UInt8 => "_UInt8",
PrimitiveKind::Int16 => "_Int16",
PrimitiveKind::UInt16 => "_UInt16",
PrimitiveKind::Int32 => INT32,
PrimitiveKind::UInt32 => "_UInt32",
PrimitiveKind::Int64 => "_Int64",
PrimitiveKind::UInt64 => "_UInt64",
PrimitiveKind::Float32 => "_Float32",
PrimitiveKind::Float64 => "_Float64",
PrimitiveKind::Float128 => "_Float128",
}
}
const INT32: &str = "_Int32";
const CHAR: &str = "_Char";
const WCHAR: &str = "_WChar";
const fn char_bits(width: CharWidth) -> i128 {
match width {
CharWidth::Char => 8,
CharWidth::WChar => 16,
}
}
fn bound_of(ast: &Ast, bound: Bound, at: &str) -> Result<i128, MaterialiseError> {
match bound.expr() {
None => Ok(0),
Some(expr) => ast
.eval_integral(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "bound".to_string(),
at: at.to_string(),
}),
}
}
fn native_type() -> Type {
Type::name("object")
}
fn native_note(ast: &Ast, id: Id<TypeNode>) -> String {
let mapping = ast
.any_of_type(id)
.ok()
.and_then(|any| ast.facets(any))
.and_then(|facets| facets.corba.as_ref())
.and_then(|corba| corba.native_mapping.clone());
match mapping {
Some(spelling) => format!(
" The source named the mapping `{spelling}`, which this module does not import."
),
None => String::new(),
}
}
fn qualified(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
let node = ast.type_(id).map_err(|_| MaterialiseError::Unevaluated {
construct: "type".to_string(),
at: "the module root".to_string(),
})?;
let mut segments = Vec::new();
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(escape_name(&node.name));
Ok(Path::new(
&segments.iter().map(String::as_str).collect::<Vec<&str>>(),
))
}
pub fn declared_path(ast: &Ast, id: Id<TypeNode>) -> Result<Path, MaterialiseError> {
qualified(ast, id)
}
fn bound_tag(ast: &Ast, bound: &zerodds_ast::Bound) -> String {
match bound.expr() {
None => String::new(),
Some(expr) => match ast.eval_integral(expr) {
Ok(limit) => format!("_b{limit}"),
Err(_) => "_bx".to_string(),
},
}
}
pub fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> Result<String, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type, whose codec name nests past {MAX_TYPE_DEPTH}"),
at: "the module root".to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok("unknown".to_string());
};
Ok(match &node.kind {
TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
TypeKind::String {
width: CharWidth::Char,
bound,
} => format!("string{}", bound_tag(ast, bound)),
TypeKind::String {
width: CharWidth::WChar,
bound,
} => format!("wstring{}", bound_tag(ast, bound)),
TypeKind::Fixed { digits, scale } => format!("fixed_{digits}_{scale}"),
TypeKind::Sequence { element, bound } => match element.resolved() {
Some(element) => format!(
"seq{}_{}",
bound_tag(ast, bound),
type_tag(ast, element, depth + 1)?
),
None => format!("seq_unknown{}", bound_tag(ast, bound)),
},
TypeKind::Array { element, dims } => {
let mut name = match element.resolved() {
Some(element) => format!("arr_{}", type_tag(ast, element, depth + 1)?),
None => "arr_unknown".to_string(),
};
for dim in dims {
match ast.eval_integral(*dim) {
Ok(length) => name.push_str(&format!("_{length}")),
Err(_) => name.push_str("_x"),
}
}
name
}
TypeKind::Map { key, value, bound } => {
let half = |half: Option<Id<TypeNode>>| match half {
Some(target) => type_tag(ast, target, depth + 1),
None => Ok("unknown".to_string()),
};
format!(
"map{}_{}_{}",
bound_tag(ast, bound),
half(key.resolved())?,
half(value.resolved())?
)
}
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(target) => type_tag(ast, target, depth + 1)?,
None => "unknown".to_string(),
},
TypeKind::ValueBox { boxed } => match boxed.resolved() {
Some(target) => format!("box_{}", type_tag(ast, target, depth + 1)?),
None => "box_unknown".to_string(),
},
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native => {
let mut segments = Vec::new();
for (_, name) in module_chain(ast, node.module)? {
segments.push(name);
}
segments.push(escape_name(&node.name));
segments.join("_")
}
TypeKind::Any => "any".to_string(),
TypeKind::ValueBase => "valuebase".to_string(),
TypeKind::ObjectRef { .. } => "objectref".to_string(),
TypeKind::AnnotationType { .. } => "annotation".to_string(),
})
}
fn alias(name: &'static str) -> Type {
Type::name(name)
}
fn annotated(base: Type, marker: Expr) -> Type {
Type::Annotated {
base: Box::new(base),
markers: vec![marker],
}
}
fn optional(inner: Type) -> Type {
Type::Optional(Box::new(inner))
}
fn call(name: &str, args: Vec<Arg>) -> Expr {
Expr::Call {
func: Path::single(name),
args,
}
}
fn kw(name: &str, value: Expr) -> Arg {
Arg::Keyword(Keyword {
name: Ident::new(name),
value,
})
}
fn bit_width(bits: u16) -> Type {
annotated(
Type::name("int"),
call(
"_IdlInt",
vec![
kw("bits", Expr::int(i128::from(bits))),
kw("signed", Expr::Literal(Literal::Bool(false))),
],
),
)
}
fn field(name: &str, type_: Type, is_optional: bool) -> Statement {
let escaped = escape_name(name);
let renamed = escaped != name;
let type_ = if is_optional { optional(type_) } else { type_ };
let value = match (is_optional, renamed) {
(false, false) => None,
(true, false) => Some(Expr::Literal(Literal::Null)),
(_, true) => {
let mut args = Vec::new();
if is_optional {
args.push(kw("default", Expr::Literal(Literal::Null)));
}
args.push(kw(
"metadata",
Expr::Dict(vec![(
Expr::Literal(Literal::string("idl")),
Expr::Literal(Literal::string(name)),
)]),
));
Some(Expr::Call {
func: Path::new(&["dataclasses", "field"]),
args,
})
}
};
Statement::Annotated(Annotated {
doc: Doc::none(),
name: Ident::new(&escaped),
type_,
value,
})
}
fn dataclass(name: &str, body: Vec<Statement>, doc: Doc) -> Statement {
let escaped = escape_name(name);
let mut doc = doc;
if escaped != name && doc.is_empty() {
doc = Doc::line(&format!("IDL name: {name}"));
} else if escaped != name {
doc.push("");
doc.push(&format!("IDL name: {name}"));
}
Statement::Class(Class {
doc,
decorators: vec![Decorator {
path: Path::new(&["dataclasses", "dataclass"]),
args: vec![kw("kw_only", Expr::Literal(Literal::Bool(true)))],
}],
name: Ident::new(&escaped),
bases: Vec::new(),
body: if body.is_empty() {
vec![Statement::Pass]
} else {
body
},
})
}
fn enum_class(name: &str, base: &str, body: Vec<Statement>, doc: Doc) -> Statement {
let escaped = escape_name(name);
let mut doc = doc;
if escaped != name {
doc.push("");
doc.push(&format!("IDL name: {name}"));
}
Statement::Class(Class {
doc,
decorators: Vec::new(),
name: Ident::new(&escaped),
bases: vec![Type::Name(Path::new(&["enum", base]))],
body: if body.is_empty() {
vec![Statement::Pass]
} else {
body
},
})
}
fn renamed_note(name: &str) -> Doc {
if escape_name(name) == name {
return Doc::none();
}
Doc::line(&format!("IDL name: {name}"))
}
fn prelude() -> Vec<Statement> {
let mut out = vec![
marker(
"_IdlInt",
&[("bits", "int"), ("signed", "bool")],
Doc::lines(&[
"The IDL integer an int annotation stands for.",
"",
"A Python int is arbitrary precision and an IDL long is 32 bits.",
"Nothing in Python enforces the difference, so it is stated here",
"instead: read it back with typing.get_type_hints(cls,",
"include_extras=True).",
]),
),
marker(
"_IdlFloat",
&[("bits", "int")],
Doc::lines(&[
"The IDL floating-point type a float or str annotation stands for.",
"",
"bits=128 goes with a str and not a float: a Python float is a",
"binary64 and would drop sixty bits of the significand at the",
"assignment.",
]),
),
marker(
"_IdlText",
&[("bits", "int"), ("bound", "int")],
Doc::lines(&[
"An IDL string, wstring, char or wchar, as a Python str.",
"",
"bits is the width of one character on the wire, 8 or 16. bound is",
"the declared maximum length, or 0 for an unbounded one.",
]),
),
marker(
"_IdlFixed",
&[("digits", "int"), ("scale", "int")],
Doc::lines(&[
"An IDL fixed<digits, scale>, as the decimal text.",
"",
"Not a decimal.Decimal: Decimal('0.00') keeps one digit and a",
"scale, so the digit count the FixedDigitCount axis measures would",
"be the container's answer rather than the source's.",
]),
),
marker(
"_IdlSequence",
&[("bound", "int")],
Doc::line("The declared maximum length of a bounded sequence or map."),
),
marker(
"_IdlEnum",
&[("declared", "type[enum.Enum]"), ("bits", "int")],
Doc::lines(&[
"The enumeration or bitmask an int annotation stands for.",
"",
"The member is an int because a writer may send a value this",
"declaration does not list, which enum.IntEnum would refuse.",
]),
),
];
for (name, bits, signed) in [
("_Int8", 8, true),
("_UInt8", 8, false),
("_Int16", 16, true),
("_UInt16", 16, false),
("_Int32", 32, true),
("_UInt32", 32, false),
("_Int64", 64, true),
("_UInt64", 64, false),
] {
out.push(Statement::Assign(Assign {
doc: Doc::none(),
name: Ident::new(name),
value: type_value(annotated(
Type::name("int"),
call(
"_IdlInt",
vec![
kw("bits", Expr::int(bits)),
kw("signed", Expr::Literal(Literal::Bool(signed))),
],
),
)),
}));
}
for (name, base, bits) in [
("_Float32", "float", 32),
("_Float64", "float", 64),
("_Float128", "str", 128),
] {
out.push(Statement::Assign(Assign {
doc: Doc::none(),
name: Ident::new(name),
value: type_value(annotated(
Type::name(base),
call("_IdlFloat", vec![kw("bits", Expr::int(bits))]),
)),
}));
}
for (name, bits) in [(CHAR, 8), (WCHAR, 16)] {
out.push(Statement::Assign(Assign {
doc: Doc::none(),
name: Ident::new(name),
value: type_value(annotated(
Type::name("str"),
call(
"_IdlText",
vec![kw("bits", Expr::int(bits)), kw("bound", Expr::int(1))],
),
)),
}));
}
out
}
fn marker(name: &str, fields: &[(&str, &str)], doc: Doc) -> Statement {
Statement::Class(Class {
doc,
decorators: vec![Decorator {
path: Path::new(&["dataclasses", "dataclass"]),
args: vec![kw("frozen", Expr::Literal(Literal::Bool(true)))],
}],
name: Ident::new(name),
bases: Vec::new(),
body: fields
.iter()
.map(|(field_name, type_name)| {
Statement::Annotated(Annotated {
doc: Doc::none(),
name: Ident::new(field_name),
type_: Type::Name(Path::new(&type_name.split('.').collect::<Vec<&str>>())),
value: None,
})
})
.collect(),
})
}
fn type_value(type_: Type) -> Expr {
Expr::Annotation(Box::new(type_))
}
fn scalar_kind(ast: &Ast, id: Id<TypeNode>) -> Option<PrimitiveKind> {
let mut current = id;
for _ in 0..MAX_TYPE_DEPTH {
let node = ast.type_(current).ok()?;
match &node.kind {
TypeKind::Primitive(kind) => return Some(*kind),
TypeKind::Alias { aliased } => current = aliased.resolved()?,
_ => return None,
}
}
None
}
fn constant_expr(
kind: Option<PrimitiveKind>,
value: &zerodds_ast::ConstValue,
at: &str,
) -> Result<Expr, MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: String| MaterialiseError::NoLanguageForm {
construct,
language: "Python",
at: at.to_string(),
};
match value {
ConstValue::Integer(number) => {
let (low, high, name) = integer_range(kind);
Ok(Expr::int(in_range(*number, low, high, name, at)?))
}
ConstValue::Boolean(flag) => Ok(Expr::Literal(Literal::Bool(*flag))),
ConstValue::Char(character) => Ok(Expr::Literal(Literal::Char(*character))),
ConstValue::WChar(point) => match constant::wchar_char(*point) {
Some(character) => Ok(Expr::Literal(Literal::Char(character))),
None => Err(refuse(format!(
"the code point U+{point:04X}, which is not a scalar value"
))),
},
ConstValue::Float { mantissa, exponent } => {
let text = constant::float_text(mantissa, exponent.as_deref());
if matches!(kind, Some(PrimitiveKind::Float128)) {
return Ok(Expr::Literal(Literal::Str(text)));
}
Ok(Expr::Literal(Literal::Float(text)))
}
ConstValue::Fixed { digits, scale } => Ok(Expr::Literal(Literal::Str(
constant::fixed_decimal(digits, *scale),
))),
ConstValue::String(text) => Ok(Expr::Literal(Literal::Str(text.clone()))),
ConstValue::WString(points) => match constant::wstring_utf8(points) {
Some(text) => Ok(Expr::Literal(Literal::Str(text))),
None => Err(refuse(String::from(
"a `wstring` constant holding a lone surrogate",
))),
},
_ => Err(refuse(String::from(
"a constant form this binding has no literal for",
))),
}
}
const fn integer_range(kind: Option<PrimitiveKind>) -> (i128, i128, &'static str) {
match kind {
Some(PrimitiveKind::Int8) => (-128, 127, "int8"),
Some(PrimitiveKind::Octet | PrimitiveKind::UInt8) => (0, 255, "octet"),
Some(PrimitiveKind::Int16) => (i16::MIN as i128, i16::MAX as i128, "short"),
Some(PrimitiveKind::UInt16) => (0, u16::MAX as i128, "unsigned short"),
Some(PrimitiveKind::Int32) => (i32::MIN as i128, i32::MAX as i128, "long"),
Some(PrimitiveKind::UInt32) => (0, u32::MAX as i128, "unsigned long"),
Some(PrimitiveKind::UInt64) => (0, u64::MAX as i128, "unsigned long long"),
Some(PrimitiveKind::Char) => (0, 255, "char"),
Some(PrimitiveKind::WChar) => (0, u16::MAX as i128, "wchar"),
Some(PrimitiveKind::Boolean) => (0, 1, "boolean"),
_ => (i64::MIN as i128, i64::MAX as i128, "long long"),
}
}
fn in_range(
value: i128,
low: i128,
high: i128,
type_: &str,
at: &str,
) -> Result<i128, MaterialiseError> {
if value < low || value > high {
return Err(MaterialiseError::NoLanguageForm {
construct: format!("the value {value}, which an IDL `{type_}` does not hold"),
language: "Python",
at: at.to_string(),
});
}
Ok(value)
}
fn named(name: &str, node: NodeId) -> String {
if name.is_empty() {
format!("{node:?}")
} else {
name.to_string()
}
}
fn ghost_created_for(applied: &Applied, node: NodeId, type_name: &str) -> Option<GhostId> {
let anchor = applied
.origin
.iter()
.find(|(_, model)| **model == node)
.map(|(ghost, _)| *ghost)?;
let typ = applied.graph.types.lookup(type_name)?;
applied
.graph
.nodes_of_type(typ)
.map(|created| created.id)
.find(|created| {
applied.graph.connected(&anchor, created)
|| applied
.graph
.parts(&anchor)
.any(|part| applied.graph.connected(&part.other, created))
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::rule;
use crate::{ModelGraph, apply};
use std::path::{Path as FsPath, PathBuf};
fn render(idl: &str, path: &str) -> Result<String, String> {
let parsed = zerodds_idl_parse::parse(idl);
if parsed.has_errors() {
return Err(format!("parse: {:?}", parsed.diagnostics()));
}
let lowered = zerodds_idl_lower::lower(&parsed.syntax(), path);
if !lowered.diagnostics.is_empty() {
return Err(format!("lower: {:?}", lowered.diagnostics));
}
let mut ast = lowered.ast;
ast.resolve_all_by_name()
.map_err(|error| format!("resolve: {error}"))?;
let applied = apply(&ModelGraph::of(&ast), &rule::python::catalogue())
.map_err(|error| format!("apply: {error}"))?;
let module =
python_module(&ast, &applied).map_err(|error| format!("materialise: {error}"))?;
Ok(zerodds_lang_render::python::module(&module))
}
fn declarations(idl: &str) -> String {
match render(idl, "test.idl") {
Ok(text) => text,
Err(why) => panic!("{why}"),
}
}
fn corpus() -> Vec<(String, PathBuf)> {
let root = FsPath::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/interop-corpus");
let mut out = Vec::new();
for entry in std::fs::read_dir(&root).expect("the corpus directory") {
let entry = entry.expect("a corpus entry");
let idl = entry.path().join("type.idl");
if idl.is_file() {
out.push((entry.file_name().to_string_lossy().into_owned(), idl));
}
}
out.sort();
out
}
#[test]
fn every_corpus_case_renders_or_names_its_failure() {
let cases = corpus();
assert!(
cases.len() > 300,
"the corpus is not where it was: {} cases",
cases.len()
);
let out = std::env::var("ZD_PYTHON_OUT").ok();
let mut failures = Vec::new();
for (name, path) in cases {
let source = std::fs::read_to_string(&path).expect("a corpus source");
match render(&source, &path.display().to_string()) {
Ok(text) => {
if let Some(dir) = &out {
let case = FsPath::new(dir).join(&name);
std::fs::create_dir_all(&case).expect("the output directory");
std::fs::write(case.join("zd_types.py"), text).expect("the output file");
}
}
Err(why) => failures.push(format!("{name}: {why}")),
}
}
let failures = crate::adapter::without_the_front_end_refusals(failures);
assert!(failures.is_empty(), "{failures:#?}");
}
#[test]
fn every_idl_primitive_reaches_an_annotation_that_states_its_width() {
let generated = declarations(
"@final struct P {
boolean a; octet b; char c; wchar d;
int8 e; uint8 f; short g; unsigned short h;
long i; unsigned long j; long long k; unsigned long long l;
float m; double n; long double o;
};",
);
for expected in [
"a: bool",
"b: _UInt8",
"c: _Char",
"d: _WChar",
"e: _Int8",
"f: _UInt8",
"g: _Int16",
"h: _UInt16",
"i: _Int32",
"j: _UInt32",
"k: _Int64",
"l: _UInt64",
"m: _Float32",
"n: _Float64",
"o: _Float128",
] {
assert!(generated.contains(expected), "{expected}\n{generated}");
}
let body = generated
.split_once("class P:")
.map(|(_, rest)| rest.to_string())
.expect("the aggregate");
assert!(
!body.contains(": int\n"),
"a bare int reached a field\n{body}"
);
assert!(
!body.contains(": float\n"),
"a bare float reached a field\n{body}"
);
assert!(
!body.contains(": str\n"),
"a bare str reached a field\n{body}"
);
}
#[test]
fn every_width_alias_states_its_bits_and_its_signedness() {
let generated = declarations("@final struct P { long x; };");
assert!(
generated.contains("_Int32 = typing.Annotated[int, _IdlInt(bits=32, signed=True)]"),
"{generated}"
);
assert!(
generated.contains("_UInt64 = typing.Annotated[int, _IdlInt(bits=64, signed=False)]"),
"{generated}"
);
}
#[test]
fn a_long_double_is_the_decimal_text_and_never_a_python_float() {
let generated = declarations("@final struct P { long double x; };");
assert!(
generated.contains("_Float128 = typing.Annotated[str, _IdlFloat(bits=128)]"),
"{generated}"
);
assert!(generated.contains("x: _Float128"), "{generated}");
}
#[test]
fn a_sequence_is_a_list_and_an_array_is_a_tuple_of_its_length() {
let generated = declarations(
"@final struct P { sequence<long> s; long a[3]; long g[2][3]; sequence<string> t; };",
);
assert!(generated.contains("s: list[_Int32]"), "{generated}");
assert!(
generated.contains("a: tuple[_Int32, _Int32, _Int32]"),
"{generated}"
);
assert!(
generated
.contains("g: tuple[tuple[_Int32, _Int32, _Int32], tuple[_Int32, _Int32, _Int32]]"),
"{generated}"
);
assert!(
generated.contains("t: list[typing.Annotated[str,"),
"{generated}"
);
}
#[test]
fn a_bound_reaches_the_annotation_and_an_unbounded_one_leaves_it_alone() {
let generated = declarations(
"@final struct P { sequence<long, 5> s; sequence<long> u; string<7> t; };",
);
assert!(
generated.contains("s: typing.Annotated[list[_Int32], _IdlSequence(bound=5)]"),
"{generated}"
);
assert!(generated.contains("u: list[_Int32]"), "{generated}");
assert!(
generated.contains("t: typing.Annotated[str, _IdlText(bits=8, bound=7)]"),
"{generated}"
);
}
#[test]
fn a_map_is_a_list_of_pairs_and_never_a_dict() {
let generated = declarations("@final struct P { map<long, string> m; };");
assert!(generated.contains("m: list[tuple[_Int32,"), "{generated}");
assert!(!generated.contains("dict["), "{generated}");
}
#[test]
fn an_optional_member_is_optional_and_defaults_to_none() {
let generated =
declarations("@mutable struct P { @optional long a; @optional string b; long c; };");
assert!(
generated.contains("a: typing.Optional[_Int32] = None"),
"{generated}"
);
assert!(
generated.contains("b: typing.Optional[typing."),
"{generated}"
);
assert!(generated.contains("c: _Int32\n"), "{generated}");
}
#[test]
fn a_dataclass_is_keyword_only_because_the_defaults_are_not_in_idl_order() {
let generated = declarations("@mutable struct P { @optional long a; long b; };");
assert!(
generated.contains("@dataclasses.dataclass(kw_only=True)"),
"{generated}"
);
}
#[test]
fn an_enumeration_is_an_intenum_and_a_member_of_it_is_an_int() {
let generated = declarations(
"enum Colour { RED, GREEN, BLUE };
@final struct P { Colour c; };",
);
assert!(
generated.contains("class Colour(enum.IntEnum):"),
"{generated}"
);
assert!(generated.contains("RED = 0"), "{generated}");
assert!(generated.contains("BLUE = 2"), "{generated}");
assert!(
generated.contains("c: typing.Annotated[int, _IdlEnum(declared=Colour, bits=32)]"),
"{generated}"
);
}
#[test]
fn an_explicit_enumerator_value_is_written_as_the_source_wrote_it() {
let generated = declarations("enum E { @value(-1) LOW, @value(70000) HIGH };");
assert!(generated.contains("LOW = -1"), "{generated}");
assert!(generated.contains("HIGH = 70000"), "{generated}");
}
#[test]
fn a_bitmask_is_an_intflag_and_a_wide_shift_needs_no_suffix() {
let generated =
declarations("@bit_bound(64) bitmask Big { @position(0) A, @position(40) B };");
assert!(
generated.contains("class Big(enum.IntFlag):"),
"{generated}"
);
assert!(generated.contains("B = (1 << 40)"), "{generated}");
}
#[test]
fn a_bitset_states_the_declared_bit_width_and_rounds_nothing_up() {
let generated = declarations(
"bitset Packed { bitfield<3> low; bitfield<5> high; };
@final struct P { Packed value; };",
);
assert!(
generated.contains("low: typing.Annotated[int, _IdlInt(bits=3, signed=False)]"),
"{generated}"
);
assert!(
generated.contains("high: typing.Annotated[int, _IdlInt(bits=5, signed=False)]"),
"{generated}"
);
assert!(generated.contains("value: Packed"), "{generated}");
}
#[test]
fn a_fixed_is_a_str_and_never_a_decimal() {
let generated = declarations(
"typedef fixed<9,3> Money;
@final struct P { Money amount; };",
);
assert!(
generated.contains("amount: typing.Annotated[str, _IdlFixed(digits=9, scale=3)]"),
"{generated}"
);
assert!(!generated.contains("import decimal"), "{generated}");
assert!(!generated.contains("amount: decimal"), "{generated}");
}
#[test]
fn a_union_is_a_dataclass_with_a_discriminator_and_one_optional_field_per_case() {
let generated = declarations(
"union Choice switch (long) {
case 1: long whole;
case 2: long other;
default: double fraction;
};
@final struct P { Choice c; };",
);
assert!(generated.contains("_d: _Int32"), "{generated}");
assert!(
generated.contains("whole: typing.Optional[_Int32] = None"),
"{generated}"
);
assert!(
generated.contains("other: typing.Optional[_Int32] = None"),
"{generated}"
);
assert!(
generated.contains("fraction: typing.Optional[_Float64] = None"),
"{generated}"
);
assert!(generated.contains("c: Choice"), "{generated}");
}
#[test]
fn a_typedef_declares_nothing_and_says_why() {
let generated = declarations(
"typedef sequence<long> Row;
@final struct P { Row r; };",
);
assert!(generated.contains("r: list[_Int32]"), "{generated}");
assert!(
generated.contains("typedef list[_Int32] Row;"),
"{generated}"
);
}
#[test]
fn a_module_is_a_nested_class_and_a_reference_is_written_from_the_root() {
let generated = declarations(
"module a { @final struct P { long x; }; };
module b { @final struct P { double y; }; };
@final struct Q { a::P one; b::P two; };",
);
assert!(generated.contains("class a:"), "{generated}");
assert!(generated.contains("class b:"), "{generated}");
assert!(generated.contains("one: a.P"), "{generated}");
assert!(generated.contains("two: b.P"), "{generated}");
}
#[test]
fn a_module_reopened_beside_a_nested_one_is_one_class() {
let generated = declarations(
"module common {
module util { @final struct Location { double x; }; };
@final struct Robot { ::common::util::Location where_; };
};",
);
assert_eq!(generated.matches("class common:").count(), 1, "{generated}");
assert!(
generated.contains("where_: common.util.Location"),
"{generated}"
);
}
#[test]
fn an_identifier_that_is_a_python_keyword_is_escaped_and_the_idl_name_is_kept() {
let generated = declarations(
"module class {
enum type { match, lambda, virtual };
@final struct P { long global; };
};",
);
assert!(generated.contains("class class_:"), "{generated}");
assert!(
generated.contains("class type_(enum.IntEnum):"),
"{generated}"
);
assert!(generated.contains("match_ = 0"), "{generated}");
assert!(generated.contains("lambda_ = 1"), "{generated}");
assert!(generated.contains("virtual = 2"), "{generated}");
assert!(
generated
.contains("global_: _Int32 = dataclasses.field(metadata={\"idl\": \"global\"})"),
"{generated}"
);
assert!(generated.contains("IDL name: match"), "{generated}");
}
#[test]
fn a_member_named_after_a_builtin_the_annotations_use_is_escaped_too() {
let generated = declarations("@mutable struct P { @optional long int; long list; };");
assert!(generated.contains("int_:"), "{generated}");
assert!(generated.contains("list_:"), "{generated}");
}
#[test]
fn two_names_that_would_escape_onto_one_are_refused() {
let failure = render("@final struct P { long class; long class_; };", "test.idl")
.expect_err("two members escaping onto one name are refused");
assert!(failure.contains("class_"), "{failure}");
}
#[test]
fn a_construct_with_no_python_data_form_is_named_and_not_left_out() {
for (idl, construct) in [
("@final struct P { any a; long tail; };", "any"),
("@final struct P { Object o; long tail; };", "Object"),
] {
let failure = render(idl, "test.idl").expect_err(construct);
assert!(failure.contains(construct), "{failure}");
assert!(failure.contains("Python"), "{failure}");
}
}
#[test]
fn a_constant_outside_its_idl_range_is_refused_although_python_would_hold_it() {
let failure = render("const octet X = 300;", "test.idl")
.expect_err("an octet constant of 300 is refused");
assert!(failure.contains("300"), "{failure}");
assert!(failure.contains("octet"), "{failure}");
}
#[test]
fn a_constant_is_final_and_carries_its_idl_type() {
let generated = declarations("const long MAX = 5; const string NAME = \"z\";");
assert!(
generated.contains("MAX: typing.Final[_Int32] = 5"),
"{generated}"
);
assert!(
generated.contains("NAME: typing.Final[typing.Annotated[str,"),
"{generated}"
);
}
#[test]
fn an_unevaluated_array_length_is_named_and_not_substituted() {
let failure = render("@final struct P { long a[UNKNOWN]; };", "test.idl")
.expect_err("an array whose length does not evaluate is refused");
assert!(!failure.is_empty(), "{failure}");
}
#[test]
fn an_interface_declares_nothing_and_the_rest_of_the_file_still_renders() {
let generated = declarations(
"interface Sensor { long read(); };
@final struct P { long x; };",
);
assert!(generated.contains("class P:"), "{generated}");
assert!(!generated.contains("Sensor"), "{generated}");
}
#[test]
fn an_empty_aggregate_still_has_a_body() {
let generated = declarations("@final struct P { };");
assert!(generated.contains("class P:"), "{generated}");
assert!(generated.contains("pass"), "{generated}");
}
}