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, CaseLabel, ModuleNode, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::{Id, NodeId};
use zerodds_lang_cst::c::Include;
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::cpp;
pub const FLOAT128: &str = "zerodds::wire::Float128";
#[must_use]
pub fn includes() -> Vec<Include> {
vec![
Include::system("array"),
Include::system("cstdint"),
Include::system("memory"),
Include::system("optional"),
Include::system("string"),
Include::system("utility"),
Include::system("vector"),
Include::local("zerodds/map.hpp"),
Include::local("zerodds/wire.hpp"),
]
}
pub fn cpp_items(ast: &Ast, applied: &Applied) -> Result<Vec<cpp::Decl>, MaterialiseError> {
let mut out: Vec<(Id<ModuleNode>, cpp::Decl)> = Vec::new();
let mut emitted: Vec<String> = Vec::new();
let (begin_file, end_file) = verbatim::file_slots(ast, verbatim::aliases::CPP);
for text in begin_file {
out.push((ast.root_module(), cpp::Decl::Verbatim(text)));
}
for id in ordered_types(ast) {
let Ok(node) = ast.type_(id) else { continue };
if !matches!(
node.kind,
TypeKind::Structure { .. } | TypeKind::Union { .. } | TypeKind::Valuetype { .. }
) {
continue;
}
out.push((
node.module,
cpp::Decl::Forward {
is_struct: true,
name: Ident::new(&cxx_name(&node.name)),
},
));
}
no_bits_name_collision(ast)?;
for id in ordered_types(ast) {
let start = out.len();
declare(ast, applied, id, &mut out, &mut emitted)?;
weave_verbatim(ast, id, start, &mut out);
maps_alongside(ast, id, &mut out, &emitted);
}
for (id, node) in ast.constants() {
if ghost_created_for(applied, node.node_id, "CppConst").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 declared = match node.type_.resolved() {
Some(target) => cpp_value_type(ast, target, &node.name)?,
None => {
return Err(MaterialiseError::Unevaluated {
construct: "constant's type".to_string(),
at: node.name.clone(),
});
}
};
let (type_, literal) = cpp_constant(&value, declared, &node.name)?;
out.push((
node.module,
cpp::Decl::Variable(cpp::Variable {
doc: Doc::none(),
type_,
name: Ident::new(&cxx_name(&node.name)),
value: Some(cpp::Expr::Literal(literal)),
is_static: false,
is_constexpr: true,
is_inline: true,
}),
));
}
for (id, node) in ast.exceptions() {
if ghost_created_for(applied, node.node_id, "CppError").is_none() {
continue;
}
let _ = id;
let mut fields = Vec::new();
let mut names = 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(),
});
};
names.push((cxx_name(&member_node.name), member_node.name.clone()));
fields.push(field(
cpp_value_type(ast, target, &member_node.name)?,
&member_node.name,
Doc::none(),
));
}
let exception_name = cxx_name(&node.name);
out.push((
node.module,
cpp::Decl::Class(class(
&exception_name,
fields,
Doc::line("An exception, as the class its members make."),
)),
));
out.extend(
idl_names(&exception_name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
out.push((node.module, idl_name(&exception_name, &node.name)));
}
for text in end_file {
out.push((ast.root_module(), cpp::Decl::Verbatim(text)));
}
nest_by_module(ast, out)
}
fn weave_verbatim(
ast: &Ast,
id: Id<TypeNode>,
start: usize,
out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
) {
let slots = verbatim::declaration_slots(ast, id, verbatim::aliases::CPP);
if slots.before.is_empty()
&& slots.begin.is_empty()
&& slots.end.is_empty()
&& slots.after.is_empty()
{
return;
}
let Ok(node) = ast.type_(id) else {
return;
};
let name = cxx_name(&node.name);
let mut declared = out.split_off(start);
let mut begin = slots.begin;
let mut end = slots.end;
let body = declared.iter_mut().find_map(|(_, decl)| match decl {
cpp::Decl::Class(class) if class.name.as_str() == name => Some(class),
_ => None,
});
if let Some(class) = body {
if class.sections.is_empty() {
class.sections.push(cpp::Section {
access: cpp::Access::Public,
members: Vec::new(),
});
}
if let Some(first) = class.sections.first_mut() {
for text in begin.drain(..).rev() {
first.members.insert(0, cpp::ClassMember::Verbatim(text));
}
}
if let Some(last) = class.sections.last_mut() {
for text in end.drain(..) {
last.members.push(cpp::ClassMember::Verbatim(text));
}
}
}
for text in slots.before.into_iter().chain(begin) {
out.push((node.module, cpp::Decl::Verbatim(text)));
}
out.append(&mut declared);
for text in end.into_iter().chain(slots.after) {
out.push((node.module, cpp::Decl::Verbatim(text)));
}
}
pub fn nest_by_module(
ast: &Ast,
items: Vec<(Id<ModuleNode>, cpp::Decl)>,
) -> Result<Vec<cpp::Decl>, MaterialiseError> {
let mut chained: Vec<(Vec<String>, cpp::Decl)> = Vec::with_capacity(items.len());
for (module, decl) in items {
let chain = if matches!(decl, cpp::Decl::Verbatim(_)) && module == ast.root_module() {
Vec::new()
} else {
module_chain(ast, module)?
};
chained.push((chain, decl));
}
Ok(nest(&chained, 0))
}
pub fn nest_by_module_placed(
ast: &Ast,
items: Vec<(Id<ModuleNode>, bool, cpp::Decl)>,
) -> Result<Vec<cpp::Decl>, MaterialiseError> {
let mut chained: Vec<(Vec<String>, cpp::Decl)> = Vec::with_capacity(items.len());
for (module, native, decl) in items {
let chain = if matches!(decl, cpp::Decl::Verbatim(_)) && module == ast.root_module() {
Vec::new()
} else if native {
module_chain(ast, module)?
} else {
without_native_prefix(|| module_chain(ast, module))?
};
chained.push((chain, decl));
}
Ok(nest(&chained, 0))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CppMapping {
Native,
#[default]
Psm,
Classic,
}
pub const NATIVE_NAMESPACE: &str = "zd_native";
thread_local! {
static MAPPING: std::cell::Cell<CppMapping> = const { std::cell::Cell::new(CppMapping::Psm) };
static PREFIX_SUSPENDED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[derive(Debug)]
pub struct CppMappingScope {
previous: CppMapping,
}
impl CppMappingScope {
#[must_use]
pub fn enter(mapping: CppMapping) -> Self {
let previous = MAPPING.with(|cell| cell.replace(mapping));
Self { previous }
}
}
impl Drop for CppMappingScope {
fn drop(&mut self) {
MAPPING.with(|cell| cell.set(self.previous));
}
}
#[must_use]
pub fn cpp_mapping() -> CppMapping {
MAPPING.with(std::cell::Cell::get)
}
fn native_prefix() -> Option<&'static str> {
(cpp_mapping() != CppMapping::Native && !PREFIX_SUSPENDED.with(std::cell::Cell::get))
.then_some(NATIVE_NAMESPACE)
}
pub(crate) fn module_names(
ast: &Ast,
module: Id<ModuleNode>,
) -> Result<Vec<String>, MaterialiseError> {
without_native_prefix(|| module_chain(ast, module))
}
pub(crate) fn without_native_prefix<R>(f: impl FnOnce() -> R) -> R {
let previous = PREFIX_SUSPENDED.with(|cell| cell.replace(true));
let result = f();
PREFIX_SUSPENDED.with(|cell| cell.set(previous));
result
}
const MAX_MODULE_DEPTH: usize = 64;
const MAX_TYPE_DEPTH: usize = 64;
fn module_handle(module: Id<ModuleNode>) -> String {
format!("#{}@{}", module.slot(), module.generation())
}
fn module_chain(ast: &Ast, module: Id<ModuleNode>) -> Result<Vec<String>, MaterialiseError> {
let mut out = Vec::new();
let mut current = Some(module);
let mut depth = 0usize;
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)
),
});
}
depth += 1;
let node = ast.module(id).map_err(|_| MaterialiseError::NoModule {
at: module_handle(id),
})?;
if !node.name.is_empty() {
out.push(cxx_name(&node.name));
}
current = node.parent;
}
out.reverse();
if let Some(prefix) = native_prefix() {
out.insert(0, prefix.to_string());
}
Ok(out)
}
fn nest(items: &[(Vec<String>, cpp::Decl)], depth: usize) -> Vec<cpp::Decl> {
let mut out = Vec::new();
let mut index = 0;
while index < items.len() {
let Some((chain, decl)) = items.get(index) else {
break;
};
let Some(name) = chain.get(depth) else {
out.push(decl.clone());
index += 1;
continue;
};
let name = name.clone();
let start = index;
while items
.get(index)
.is_some_and(|(chain, _)| chain.get(depth) == Some(&name))
{
index += 1;
}
out.push(cpp::Decl::Namespace(cpp::Namespace {
doc: Doc::none(),
name: Ident::new(&name),
decls: nest(&items[start..index], depth + 1),
}));
}
out
}
#[must_use]
pub fn cpp_type_tag(ast: &Ast, id: Id<TypeNode>) -> String {
type_tag(ast, id, 0)
}
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(),
},
}
}
fn type_tag(ast: &Ast, id: Id<TypeNode>, depth: usize) -> String {
if depth > MAX_TYPE_DEPTH {
return "deep".to_string();
}
let Ok(node) = ast.type_(id) else {
return "void".to_string();
};
match &node.kind {
TypeKind::Primitive(kind) => kind.as_str().replace(' ', "_"),
TypeKind::String {
width: zerodds_ast::CharWidth::Char,
bound,
} => format!("string{}", bound_tag(ast, bound)),
TypeKind::String {
width: zerodds_ast::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 => "void".to_string(),
},
TypeKind::Array { element, dims } => match element.resolved() {
Some(element) => {
let mut name = format!("arr_{}", type_tag(ast, element, depth + 1));
for dim in dims {
match ast.eval_integral(*dim) {
Ok(length) => name.push_str(&format!("_{length}")),
Err(_) => name.push_str("_x"),
}
}
name
}
None => "void".to_string(),
},
TypeKind::Map { key, value, bound } => match (key.resolved(), value.resolved()) {
(Some(key), Some(value)) => format!(
"map{}_{}_{}",
bound_tag(ast, bound),
type_tag(ast, key, depth + 1),
type_tag(ast, value, depth + 1)
),
_ => "void".to_string(),
},
TypeKind::Alias { aliased } if node.name.is_empty() => match aliased.resolved() {
Some(target) => type_tag(ast, target, depth + 1),
None => "void".to_string(),
},
_ => flattened(ast, id),
}
}
#[must_use]
pub fn cxx_name(name: &str) -> String {
if cpp::escape_keyword(name) == name {
name.to_string()
} else {
format!("cxx_{name}")
}
}
#[must_use]
pub fn flattened(ast: &Ast, id: Id<TypeNode>) -> String {
let Ok(any) = ast.any_of_type(id) else {
return "zd_unnamed".to_string();
};
match ast.scoped_name_of(any) {
Some(scoped) => scoped
.segments()
.iter()
.map(|segment| cxx_name(segment))
.collect::<Vec<_>>()
.join("_"),
None => "zd_unnamed".to_string(),
}
}
#[must_use]
pub fn cpp_type_path(ast: &Ast, id: Id<TypeNode>) -> Path {
let Ok(any) = ast.any_of_type(id) else {
return Path::single("zd_unnamed");
};
let Some(scoped) = ast.scoped_name_of(any) else {
return Path::single("zd_unnamed");
};
let escaped: Vec<String> = scoped
.segments()
.iter()
.map(|segment| cxx_name(segment))
.collect();
let mut borrowed: Vec<&str> = escaped.iter().map(String::as_str).collect();
if let Some(prefix) = native_prefix() {
borrowed.insert(0, prefix);
}
Path::rooted(&borrowed)
}
pub fn cpp_value_type(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
) -> Result<cpp::Type, MaterialiseError> {
value_type(ast, id, at, 0)
}
fn value_type(
ast: &Ast,
id: Id<TypeNode>,
at: &str,
depth: usize,
) -> Result<cpp::Type, MaterialiseError> {
if depth > MAX_TYPE_DEPTH {
return Err(MaterialiseError::Unevaluated {
construct: format!("type spelling — it nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
});
}
let Ok(node) = ast.type_(id) else {
return Ok(cpp::Type::Void);
};
let type_ = match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => float128(),
TypeKind::Primitive(kind) => cpp_primitive(*kind),
TypeKind::String { .. } | TypeKind::Fixed { .. } => std_type("string", Vec::new()),
TypeKind::Sequence { element, .. } => std_type(
"vector",
vec![element_type(ast, element.resolved(), at, depth)?],
),
TypeKind::Array { element, dims } => {
let mut out = element_type(ast, element.resolved(), at, depth)?;
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(),
})?;
if length < 0 {
return Err(MaterialiseError::Unevaluated {
construct: format!("array length — it evaluates to {length}"),
at: at.to_string(),
});
}
out = std_type("array", vec![out, length_arg(length)]);
}
out
}
TypeKind::Map { key, value, bound } => {
let mut arguments = vec![
element_type(ast, key.resolved(), at, depth)?,
element_type(ast, value.resolved(), at, depth)?,
];
if let Some(expr) = bound.expr() {
let limit = ast
.eval_integral(expr)
.map_err(|_| MaterialiseError::Unevaluated {
construct: "map bound".to_string(),
at: at.to_string(),
})?;
arguments.push(length_arg(limit));
}
cpp::Type::template(Path::rooted(&["zerodds", "Map"]), arguments)
}
TypeKind::Alias { aliased } if node.name.is_empty() => {
element_type(ast, aliased.resolved(), at, depth)?
}
TypeKind::ValueBox { boxed } => std_type(
"shared_ptr",
vec![element_type(ast, boxed.resolved(), at, depth)?],
),
TypeKind::Any => return Err(no_form("any", at)),
TypeKind::ValueBase => return Err(no_form("ValueBase", at)),
TypeKind::ObjectRef { .. } => return Err(no_form("Object", at)),
TypeKind::Alias { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native
| TypeKind::AnnotationType { .. } => cpp::Type::named(cpp_type_path(ast, id)),
};
Ok(type_)
}
fn element_type(
ast: &Ast,
target: Option<Id<TypeNode>>,
at: &str,
depth: usize,
) -> Result<cpp::Type, MaterialiseError> {
match target {
Some(target) => value_type(ast, target, at, depth + 1),
None => Err(MaterialiseError::Unevaluated {
construct: "element type".to_string(),
at: at.to_string(),
}),
}
}
fn no_form(construct: &str, at: &str) -> MaterialiseError {
MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: "C++",
at: at.to_string(),
}
}
#[must_use]
pub fn cpp_primitive(kind: PrimitiveKind) -> cpp::Type {
match kind {
PrimitiveKind::Boolean => cpp::Type::Primitive(cpp::Primitive::Bool),
PrimitiveKind::Char => cpp::Type::Primitive(cpp::Primitive::Char),
PrimitiveKind::Float32 => cpp::Type::Primitive(cpp::Primitive::Float),
PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
cpp::Type::Primitive(cpp::Primitive::Double)
}
PrimitiveKind::Octet => octet(),
PrimitiveKind::UInt8 => std_type("uint8_t", Vec::new()),
PrimitiveKind::Int8 => std_type("int8_t", Vec::new()),
PrimitiveKind::WChar | PrimitiveKind::UInt16 => std_type("uint16_t", Vec::new()),
PrimitiveKind::Int16 => std_type("int16_t", Vec::new()),
PrimitiveKind::Int32 => std_type("int32_t", Vec::new()),
PrimitiveKind::UInt32 => std_type("uint32_t", Vec::new()),
PrimitiveKind::Int64 => std_type("int64_t", Vec::new()),
PrimitiveKind::UInt64 => std_type("uint64_t", Vec::new()),
}
}
#[must_use]
pub fn cpp_holder(bits: u16) -> cpp::Type {
match bits {
0..=8 => std_type("uint8_t", Vec::new()),
9..=16 => std_type("uint16_t", Vec::new()),
17..=32 => std_type("uint32_t", Vec::new()),
_ => std_type("uint64_t", Vec::new()),
}
}
fn external_doc() -> Doc {
Doc::lines(&[
"Held behind an indirection — @external, DDS-XTypes 1.3 Table 21. The",
"annotation changes no octet on the wire; it says where the binding",
"keeps the value. A `std::shared_ptr` and not the value itself,",
"because a member that reaches its own enclosing class back has no",
"size at all held inline — `std::optional<T>` and a plain `T` member",
"both require T complete, and clang answers `field has incomplete",
"type` and six `incomplete type ... used in type trait expression`",
"for `tests/interop-corpus/recursive-optional-external`. A shared_ptr",
"is complete whatever it points at.",
"",
"Where the member is also @optional the pointer is the absence too",
"(7.2.2.4.4.4.7): a null pointer is the member not being there, and a",
"std::optional around it would carry a present-but-null state the",
"wire has no encoding for.",
])
}
fn std_type(name: &str, args: Vec<cpp::Type>) -> cpp::Type {
cpp::Type::template(Path::new(&["std", name]), args)
}
fn float128() -> cpp::Type {
cpp::Type::named(Path::rooted(&["zerodds", "wire", "Float128"]))
}
fn octet() -> cpp::Type {
cpp::Type::named(Path::rooted(&["zerodds", "wire", "Octet"]))
}
fn length_arg(length: i128) -> cpp::Type {
cpp::Type::named(Path::single(&format!("{length}")))
}
fn int_literal(value: i128) -> Literal {
if value > i128::from(i64::MAX) {
return Literal::Int(format!("{value}u"));
}
Literal::Int(zerodds_lang_render::cpp::integer_text(value))
}
fn member_default(
ast: &Ast,
target: Id<TypeNode>,
spelled: &cpp::Type,
at: &str,
) -> Result<Option<cpp::Expr>, MaterialiseError> {
let mut id = target;
for _ in 0..=MAX_TYPE_DEPTH {
let Ok(node) = ast.type_(id) else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: at.to_string(),
});
};
match &node.kind {
TypeKind::Alias { aliased } => match aliased.resolved() {
Some(next) => id = next,
None => return Ok(None),
},
TypeKind::Primitive(_) | TypeKind::Array { .. } => {
return Ok(Some(cpp::Expr::InitList(Vec::new())));
}
TypeKind::Enumeration { enumerators } => {
let first = enumerators
.first()
.and_then(|first| ast.enumerator(*first).ok())
.ok_or_else(|| MaterialiseError::Unevaluated {
construct: "the first enumerator of the member's enumeration".to_string(),
at: at.to_string(),
})?;
let cpp::Type::Named { path, .. } = spelled else {
return Err(MaterialiseError::Unevaluated {
construct: "the C++ name of the member's enumeration".to_string(),
at: at.to_string(),
});
};
return Ok(Some(cpp::Expr::Path(path.join(&cxx_name(&first.name)))));
}
_ => return Ok(None),
}
}
Err(MaterialiseError::Unevaluated {
construct: format!("typedef chain — it nests past {MAX_TYPE_DEPTH}"),
at: at.to_string(),
})
}
fn no_bits_name_collision(ast: &Ast) -> Result<(), MaterialiseError> {
let mut spelled: Vec<(String, String)> = Vec::new();
for id in ordered_types(ast) {
let Ok(node) = ast.type_(id) else { continue };
if node.name.is_empty() || !has_idl_name(&node.kind) {
continue;
}
let scope = format!("{}@{}", node.module.slot(), node.module.generation());
spelled.push((
format!("{scope}::{}", cxx_name(&node.name)),
node.name.clone(),
));
if matches!(node.kind, TypeKind::Bitmask { .. }) {
spelled.push((
format!("{scope}::{}Bits", cxx_name(&node.name)),
format!("{}Bits (the flag type of bitmask {})", node.name, node.name),
));
}
}
no_spelling_collision(
spelled
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
"C++",
"the types of one header",
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Held {
Value,
Shared,
Plain,
}
pub fn held(ast: &Ast, member: &zerodds_ast::MemberNode) -> Held {
if super::annotated_external(member) {
Held::Shared
} else if member.facets.is_optional() {
Held::Plain
} else if super::held_externally(ast, member) {
Held::Shared
} else {
Held::Value
}
}
fn owning_members(
class: &str,
base: Option<&str>,
values: &[String],
pointers: &[String],
) -> Vec<cpp::ClassMember> {
if pointers.is_empty() {
return Vec::new();
}
let wire = |function: &str| cpp::Expr::Path(Path::new(&["zerodds", "wire", function]));
let call = |callee: cpp::Expr, args: Vec<cpp::Expr>| cpp::Expr::Call {
callee: Box::new(callee),
args,
};
let assign = |place: cpp::Expr, value: cpp::Expr| {
cpp::Statement::Expr(cpp::Expr::Binary {
op: "=",
lhs: Box::new(place),
rhs: Box::new(value),
})
};
let mine = |name: &str| cpp::Expr::Path(Path::single(name));
let theirs = |name: &str| cpp::Expr::Member {
value: Box::new(cpp::Expr::Path(Path::single("zd_other"))),
name: Ident::new(name),
};
let moved = |value: cpp::Expr| call(cpp::Expr::Path(Path::new(&["std", "move"])), vec![value]);
let own_type = || cpp::Type::named(Path::single(class));
let param = |type_: cpp::Type| cpp::Param {
type_,
name: Some(Ident::new("zd_other")),
default: None,
};
let const_ref = || cpp::Type::Ref(Box::new(cpp::Type::Const(Box::new(own_type()))));
let rvalue = || cpp::Type::RvalueRef(Box::new(own_type()));
let not_self = |body: Vec<cpp::Statement>| cpp::Statement::If {
condition: cpp::Expr::Binary {
op: "!=",
lhs: Box::new(cpp::Expr::Path(Path::single("this"))),
rhs: Box::new(cpp::Expr::AddressOf(Box::new(mine("zd_other")))),
},
then: cpp::Block { statements: body },
otherwise: None,
};
let give_self = || {
cpp::Statement::Return(Some(cpp::Expr::Deref(Box::new(cpp::Expr::Path(
Path::single("this"),
)))))
};
let constructor = |doc: &str,
params: Vec<cpp::Param>,
initialisers: Vec<(Ident, cpp::Expr)>,
body: cpp::FunctionBody| {
cpp::ClassMember::Constructor(cpp::Constructor {
doc: Doc::line(doc),
name: Ident::new(class),
params,
initialisers,
is_explicit: false,
is_constexpr: false,
is_noexcept: false,
body,
})
};
let operator = |doc: &str, param_type: cpp::Type, body: Vec<cpp::Statement>| {
cpp::ClassMember::Method(cpp::Method {
doc: Doc::line(doc),
returns: cpp::Type::Ref(Box::new(own_type())),
name: Ident::new("operator="),
params: vec![param(param_type)],
is_const: false,
is_static: false,
is_virtual: false,
is_pure: false,
is_constexpr: false,
is_noexcept: false,
is_override: false,
body: Some(cpp::Block { statements: body }),
})
};
let mut copy = Vec::new();
let mut copy_assign = Vec::new();
let mut move_ctor = Vec::new();
let mut move_assign = Vec::new();
let mut release = Vec::new();
let (base_copy, base_move) = base.map_or((Vec::new(), Vec::new()), |base| {
(
vec![(Ident::new(base), mine("zd_other"))],
vec![(Ident::new(base), moved(mine("zd_other")))],
)
});
if let Some(base) = base {
let assign_base = |value: cpp::Expr| {
cpp::Statement::Expr(call(
cpp::Expr::Path(Path::single(&format!("{base}::operator="))),
vec![value],
))
};
copy_assign.push(assign_base(mine("zd_other")));
move_assign.push(assign_base(moved(mine("zd_other"))));
}
for name in values {
copy.push(assign(mine(name), theirs(name)));
copy_assign.push(assign(mine(name), theirs(name)));
move_ctor.push(assign(mine(name), moved(theirs(name))));
move_assign.push(assign(mine(name), moved(theirs(name))));
}
for name in pointers {
let cloned = call(wire("clone"), vec![theirs(name)]);
let taken = call(wire("take"), vec![theirs(name)]);
let destroy = || cpp::Statement::Expr(call(wire("destroy"), vec![mine(name)]));
copy.push(assign(mine(name), cloned.clone()));
copy_assign.push(cpp::Statement::Declare {
type_: cpp::Type::Auto,
name: Ident::new(&format!("zd_new_{name}")),
value: Some(cloned),
});
copy_assign.push(destroy());
copy_assign.push(assign(mine(name), mine(&format!("zd_new_{name}"))));
move_ctor.push(assign(mine(name), taken.clone()));
move_assign.push(destroy());
move_assign.push(assign(mine(name), taken));
release.push(destroy());
}
copy_assign.sort_by_key(|statement| !matches!(statement, cpp::Statement::Declare { .. }));
vec![
constructor(
"NULL pointers (XTypes 1.3 7.5.1.2.3.2, as in C).",
Vec::new(),
Vec::new(),
cpp::FunctionBody::Defaulted,
),
constructor(
"A deep copy of what the pointer members point at.",
vec![param(const_ref())],
base_copy,
cpp::FunctionBody::Block(cpp::Block { statements: copy }),
),
constructor(
"Takes the pointers over and leaves NULL behind.",
vec![param(rvalue())],
base_move,
cpp::FunctionBody::Block(cpp::Block {
statements: move_ctor,
}),
),
operator("A deep copy.", const_ref(), {
let mut body = vec![not_self(copy_assign)];
body.push(give_self());
body
}),
operator("Takes the pointers over.", rvalue(), {
let mut body = vec![not_self(move_assign)];
body.push(give_self());
body
}),
cpp::ClassMember::Destructor(cpp::Destructor {
doc: Doc::line("Deletes what the non-NULL pointer members point at."),
name: Ident::new(class),
is_virtual: false,
body: cpp::FunctionBody::Block(cpp::Block {
statements: release,
}),
}),
]
}
fn field(type_: cpp::Type, name: &str, doc: Doc) -> cpp::ClassMember {
cpp::ClassMember::Field(cpp::Field {
doc,
type_,
name: Ident::new(&cxx_name(name)),
dims: Vec::new(),
default: None,
is_static: false,
is_constexpr: false,
bits: None,
})
}
pub(crate) const UNION_DISCRIMINATOR: &str = "zd_d";
pub(crate) fn union_storage(field: &str) -> String {
format!("zd_{field}")
}
pub(crate) struct UnionCase {
pub(crate) name: String,
pub(crate) type_: cpp::Type,
pub(crate) label: Option<String>,
pub(crate) doc: Doc,
}
pub(crate) fn union_members(
discriminator: &cpp::Type,
cases: &[UnionCase],
) -> Vec<cpp::ClassMember> {
let disc = zerodds_lang_render::cpp::type_(discriminator);
let mut text = format!(
" /// The discriminator.\n {disc} _d() const {{ return {UNION_DISCRIMINATOR}; }}\n void _d({disc} value) {{ {UNION_DISCRIMINATOR} = value; }}\n"
);
for case in cases {
let ty = zerodds_lang_render::cpp::type_(&case.type_);
let storage = union_storage(&case.name);
let select = case.label.as_ref().map_or(String::new(), |label| {
format!(" {UNION_DISCRIMINATOR} = static_cast<{disc}>({label});")
});
text.push_str(&format!(
" const {ty}& {name}() const {{ return {storage}; }}\n {ty}& {name}() {{ return {storage}; }}\n void {name}(const {ty}& value) {{ {storage} = value;{select} }}\n void {name}({ty}&& value) {{ {storage} = std::move(value);{select} }}\n",
name = case.name
));
}
let mut members = vec![cpp::ClassMember::Verbatim(text)];
members.push(field(
discriminator.clone(),
UNION_DISCRIMINATOR,
Doc::line("The storage of `_d()`. Which storage below is live is what it says."),
));
for case in cases {
members.push(cpp::ClassMember::Field(cpp::Field {
doc: case.doc.clone(),
type_: case.type_.clone(),
name: Ident::new(&union_storage(&case.name)),
dims: Vec::new(),
default: None,
is_static: false,
is_constexpr: false,
bits: None,
}));
}
members
}
const SWAP_PARAMETER: &str = "zd_other";
fn swap_member(name: &str, names: &[(String, String)]) -> cpp::ClassMember {
let statements: Vec<_> = names
.iter()
.map(|(spelled, _)| {
cpp::Statement::Expr(cpp::Expr::Call {
callee: Box::new(cpp::Expr::Path(Path::new(&["std", "swap"]))),
args: vec![
cpp::Expr::Path(Path::single(spelled)),
cpp::Expr::Member {
value: Box::new(cpp::Expr::Path(Path::single(SWAP_PARAMETER))),
name: Ident::new(spelled),
},
],
})
})
.collect();
cpp::ClassMember::Method(cpp::Method {
doc: Doc::line("Exchanges every member with `zd_other` (DDS-PSM-Cxx 1.0 7.12)."),
returns: cpp::Type::Void,
name: Ident::new("swap"),
params: vec![cpp::Param {
type_: cpp::Type::Ref(Box::new(cpp::Type::named(Path::single(name)))),
name: (!statements.is_empty()).then(|| Ident::new(SWAP_PARAMETER)),
default: None,
}],
is_const: false,
is_static: false,
is_virtual: false,
is_pure: false,
is_constexpr: false,
is_noexcept: false,
is_override: false,
body: Some(cpp::Block { statements }),
})
}
fn bitset_equality(name: &str, names: &[(String, String)]) -> cpp::ClassMember {
let (parameter, terms) = if names.is_empty() {
(String::new(), "true".to_string())
} else {
let terms: Vec<String> = names
.iter()
.map(|(spelled, _)| format!("{spelled} == {SWAP_PARAMETER}.{spelled}"))
.collect();
(format!(" {SWAP_PARAMETER}"), terms.join(" && "))
};
cpp::ClassMember::Verbatim(format!(
" bool operator==(const {name}&{parameter}) const {{ return {terms}; }}\n \
bool operator!=(const {name}& {SWAP_PARAMETER}) const {{ return !(*this == {SWAP_PARAMETER}); }}\n"
))
}
fn swap_function(name: &str) -> cpp::Decl {
let reference = || cpp::Type::Ref(Box::new(cpp::Type::named(Path::single(name))));
cpp::Decl::Function(cpp::Function {
doc: Doc::line("Exchanges two values (DDS-PSM-Cxx 1.0 7.12)."),
returns: cpp::Type::Void,
name: Ident::new("swap"),
params: vec![
cpp::Param {
type_: reference(),
name: Some(Ident::new("zd_a")),
default: None,
},
cpp::Param {
type_: reference(),
name: Some(Ident::new("zd_b")),
default: None,
},
],
is_inline: true,
is_static: false,
is_constexpr: false,
is_noexcept: false,
is_maybe_unused: false,
body: cpp::FunctionBody::Block(cpp::Block {
statements: vec![cpp::Statement::Expr(cpp::Expr::MethodCall {
receiver: Box::new(cpp::Expr::Path(Path::single("zd_a"))),
name: Ident::new("swap"),
args: vec![cpp::Expr::Path(Path::single("zd_b"))],
})],
}),
})
}
const IDL_NAMES_SUFFIX: &str = "_idl_names";
const IDL_NAME_SUFFIX: &str = "_idl_name";
fn text_pointer() -> cpp::Type {
cpp::Type::Pointer(Box::new(cpp::Type::Const(Box::new(cpp::Type::Primitive(
cpp::Primitive::Char,
)))))
}
fn idl_names(
name: &str,
pairs: &[(String, String)],
) -> Result<Option<cpp::Decl>, MaterialiseError> {
no_spelling_collision(
pairs
.iter()
.map(|(spelled, declared)| (spelled.as_str(), declared.as_str())),
"C++",
name,
)?;
if pairs.is_empty() {
return Ok(None);
}
let entries: Vec<cpp::Expr> = pairs
.iter()
.map(|(spelling, idl)| {
cpp::Expr::InitList(vec![
cpp::Expr::Literal(Literal::Str(spelling.clone())),
cpp::Expr::Literal(Literal::Str(idl.clone())),
])
})
.collect();
Ok(Some(cpp::Decl::Variable(cpp::Variable {
doc: Doc::lines(&[
"The IDL spelling of each member, beside the spelling this header",
"uses, in IDL declaration order.",
"",
"Load-bearing, not documentation. A member's ID under @autoid(HASH)",
"and under @hashid is the first four octets of the MD5 of its IDL",
"name (XTypes 1.3 7.3.1.2.1.1), so a caller holding only the C++",
"spelling — which a keyword collision moved — cannot compute the ID",
"of the member it is writing.",
]),
type_: std_type(
"array",
vec![
std_type("pair", vec![text_pointer(), text_pointer()]),
cpp::Type::named(Path::single(&pairs.len().to_string())),
],
),
name: Ident::new(&format!("{name}{IDL_NAMES_SUFFIX}")),
value: Some(cpp::Expr::InitList(vec![cpp::Expr::InitList(entries)])),
is_static: false,
is_constexpr: true,
is_inline: true,
})))
}
fn idl_name(name: &str, idl: &str) -> cpp::Decl {
cpp::Decl::Variable(cpp::Variable {
doc: Doc::line("The IDL spelling of the declaration above."),
type_: text_pointer(),
name: Ident::new(&format!("{name}{IDL_NAME_SUFFIX}")),
value: Some(cpp::Expr::Literal(Literal::Str(idl.to_string()))),
is_static: false,
is_constexpr: true,
is_inline: true,
})
}
const fn has_idl_name(kind: &TypeKind) -> bool {
matches!(
kind,
TypeKind::Structure { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. }
| TypeKind::Alias { .. }
| TypeKind::Native
| TypeKind::ValueBox { .. }
)
}
fn class(name: &str, members: Vec<cpp::ClassMember>, doc: Doc) -> cpp::Class {
let sections = if members.is_empty() {
Vec::new()
} else {
vec![cpp::Section {
access: cpp::Access::Public,
members,
}]
};
cpp::Class {
doc,
is_struct: true,
name: Ident::new(name),
bases: Vec::new(),
sections,
}
}
fn maps_alongside(
ast: &Ast,
id: Id<TypeNode>,
out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
emitted: &[String],
) {
if cpp_mapping() == CppMapping::Psm {
return;
}
let Ok(node) = ast.type_(id) else { return };
let declared = match &node.kind {
TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Enumeration { .. }
| TypeKind::Bitmask { .. }
| TypeKind::Bitset { .. } => true,
TypeKind::Alias { .. } => !node.name.is_empty(),
_ => false,
};
if !declared {
return;
}
let key = cpp_type_path(ast, id)
.segments()
.iter()
.map(Ident::as_str)
.collect::<Vec<_>>()
.join("::");
if !emitted.contains(&key) {
return;
}
let name = cxx_name(&node.name);
for (key_name, key_type) in [
("UInt32", std_type("uint32_t", Vec::new())),
("UInt64", std_type("uint64_t", Vec::new())),
("String", std_type("string", Vec::new())),
] {
out.push((
node.module,
cpp::Decl::Alias(cpp::Alias {
doc: Doc::none(),
name: Ident::new(&format!("{key_name}{name}Map")),
type_: cpp::Type::template(
Path::rooted(&["zerodds", "Map"]),
vec![key_type, cpp::Type::named(cpp_type_path(ast, id))],
),
}),
));
}
}
#[allow(clippy::too_many_lines)]
fn declare(
ast: &Ast,
applied: &Applied,
id: Id<TypeNode>,
out: &mut Vec<(Id<ModuleNode>, cpp::Decl)>,
emitted: &mut Vec<String>,
) -> Result<(), MaterialiseError> {
let Ok(node) = ast.type_(id) else {
return Ok(());
};
match &node.kind {
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Map { .. } => return Ok(()),
TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => return Ok(()),
_ => {}
}
let name = cxx_name(&node.name);
let key = cpp_type_path(ast, id)
.segments()
.iter()
.map(Ident::as_str)
.collect::<Vec<_>>()
.join("::");
if emitted.contains(&key) {
return Ok(());
}
match &node.kind {
TypeKind::Alias { aliased } => {
if node.name.is_empty() {
return Ok(());
}
if ghost_created_for(applied, node.node_id, "CppAlias").is_none() {
return Ok(());
}
let Some(target) = aliased.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "typedef target".to_string(),
at: name,
});
};
emitted.push(key);
out.push((
node.module,
cpp::Decl::Alias(cpp::Alias {
doc: Doc::none(),
name: Ident::new(&name),
type_: cpp_value_type(ast, target, &name)?,
}),
));
}
TypeKind::Enumeration { enumerators } => {
if ghost_created_for(applied, node.node_id, "CppEnum").is_none() {
return Ok(());
}
let mut values = Vec::new();
let mut names = 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, "CppEnumerator").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(),
}
})?;
values.push((cxx_name(&enum_node.name), value));
names.push((cxx_name(&enum_node.name), enum_node.name.clone()));
}
emitted.push(key);
out.push((
node.module,
cpp::Decl::Enum(cpp::Enum {
doc: Doc::lines(&[
"An enumeration. The underlying type is chosen from the",
"@bit_bound and from the values themselves — a C enumeration",
"constant is an `int` and could hold neither end of",
"@bit_bound(32), which is why the C column emits a typedef and",
"a run of #defines instead.",
"",
"Scoped, so the enumerators do not leak into the namespace. IDL",
"4.2 7.4.1.4.2 names them from the scope enclosing the",
"enumeration — `M::medium`, never `M::Size::medium` — and this",
"is the one place the C++ mapping does not reproduce the IDL",
"spelling: an unscoped `enum` would put every enumerator of",
"every enumeration of a module in one namespace, where two",
"IDL-legal names can collide.",
]),
is_scoped: true,
name: Ident::new(&name),
underlying: Some(enum_underlying(ast.effective_bit_bound(id), &values)),
enumerators: values
.into_iter()
.map(|(name, value)| cpp::Enumerator {
doc: Doc::none(),
name: Ident::new(&name),
value: Some(cpp::Expr::Literal(int_literal(value))),
})
.collect(),
}),
));
out.extend(
idl_names(&name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
}
TypeKind::Bitmask { bits } => {
if ghost_created_for(applied, node.node_id, "CppBitmask").is_none() {
return Ok(());
}
emitted.push(key);
let bound = ast.effective_bit_bound(id);
let mut names = Vec::new();
let mut flag_values: Vec<(String, i128)> = Vec::new();
out.push((
node.module,
cpp::Decl::Alias(cpp::Alias {
doc: Doc::line(
"A bitmask, as the unsigned integer Table 33 of XTypes 1.3 \
7.4.1.1.3.2 gives for its bound.",
),
name: Ident::new(&name),
type_: cpp_holder(bound),
}),
));
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, "CppBitConst").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(),
}
})?;
let one = cpp::Expr::Construct {
type_: Box::new(cpp::Type::named(cpp_type_path(ast, id))),
args: vec![cpp::Expr::Literal(Literal::int(1))],
braced: true,
};
names.push((cxx_name(&bit_node.name), bit_node.name.clone()));
out.push((
node.module,
cpp::Decl::Variable(cpp::Variable {
doc: Doc::none(),
type_: cpp::Type::named(cpp_type_path(ast, id)),
name: Ident::new(&cxx_name(&bit_node.name)),
value: Some(cpp::Expr::Cast {
kind: cpp::CastKind::Static,
type_: Box::new(cpp::Type::named(cpp_type_path(ast, id))),
value: Box::new(cpp::Expr::Paren(Box::new(cpp::Expr::Binary {
op: "<<",
lhs: Box::new(one),
rhs: Box::new(cpp::Expr::Literal(Literal::int(i128::from(
position,
)))),
}))),
}),
is_static: false,
is_constexpr: true,
is_inline: true,
}),
));
flag_values.push((cxx_name(&bit_node.name), 1i128 << position));
}
out.push((
node.module,
cpp::Decl::Enum(cpp::Enum {
doc: Doc::line(
"The enumeration of the flags of a bitmask (XTypes 1.3 \
7.5.1.2.2): one enumerator per flag, valued 1 << position.",
),
is_scoped: true,
name: Ident::new(&format!("{name}Bits")),
underlying: Some(cpp_holder(bound)),
enumerators: flag_values
.into_iter()
.map(|(flag, value)| cpp::Enumerator {
doc: Doc::none(),
name: Ident::new(&flag),
value: Some(cpp::Expr::Literal(int_literal(value))),
})
.collect(),
}),
));
out.extend(
idl_names(&name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
}
TypeKind::Bitset { .. } => {
if ghost_created_for(applied, node.node_id, "CppBitset").is_none() {
return Ok(());
}
emitted.push(key);
let mut members = Vec::new();
let mut names = 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;
};
names.push((cxx_name(field_name), field_name.clone()));
members.push(field(cpp_holder(field_node.bits), field_name, Doc::none()));
}
members.push(bitset_equality(&name, &names));
out.push((
node.module,
cpp::Decl::Class(class(
&name,
members,
Doc::line(
"A bitset. The fields are separate integers here; the packing \
into the anchor is the codec's, because C++ leaves bit-field \
allocation order to the implementation.",
),
)),
));
out.extend(
idl_names(&name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let created = if matches!(node.kind, TypeKind::Valuetype { .. }) {
"CppValue"
} else {
"CppStruct"
};
if ghost_created_for(applied, node.node_id, created).is_none() {
return Ok(());
}
emitted.push(key);
let base = match &node.kind {
TypeKind::Structure {
base: Some(base), ..
} => base.resolved(),
_ => None,
};
let own: Vec<_> = ast.members_of(id).to_vec();
let mut members = Vec::new();
let mut names = Vec::new();
let mut values: Vec<String> = Vec::new();
let mut pointers: Vec<String> = Vec::new();
for member in ast.all_members_of(id) {
let Ok(member_node) = ast.member(member) else {
return Err(MaterialiseError::Unevaluated {
construct: "member the model listed".to_string(),
at: node.name.clone(),
});
};
names.push((cxx_name(&member_node.name), member_node.name.clone()));
if base.is_some() && !own.contains(&member) {
continue;
}
let Some(target) = member_node.type_.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "member type".to_string(),
at: member_node.name.clone(),
});
};
let inner = cpp_value_type(ast, target, &member_node.name)?;
match held(ast, member_node) {
Held::Shared => {
members.push(field(
std_type("shared_ptr", vec![inner]),
&member_node.name,
external_doc(),
));
pointers.push(cxx_name(&member_node.name));
}
Held::Plain => {
let mut member = field(
cpp::Type::Pointer(Box::new(inner)),
&member_node.name,
Doc::lines(&[
"An @optional member: a plain pointer, NULL for an omitted",
"value. DDS-XTypes 1.3 7.5.1.2.4.2: \"Optional members shall be",
"represented using plain pointers rather than automatic values",
"or smart pointers [...] A NULL pointer shall indicate an",
"omitted value.\" The class owns what it points at: the",
"destructor deletes it and a copy is deep, as 7.5.1.2.3.2 asks",
"of an external member. In a @mutable type an absent member is",
"not written at all; outside one its absence is written — a",
"zero <is_present> byte under XCDR2 (rule 20) or a parameter",
"header declaring a length of zero under XCDR1 (7.4.1.2).",
]),
);
if let cpp::ClassMember::Field(f) = &mut member {
f.default = Some(cpp::Expr::Path(Path::new(&["nullptr"])));
}
members.push(member);
pointers.push(cxx_name(&member_node.name));
}
Held::Value => {
let default = member_default(ast, target, &inner, &member_node.name)?;
let mut member = field(inner, &member_node.name, Doc::none());
if let cpp::ClassMember::Field(f) = &mut member {
f.default = default;
}
members.push(member);
values.push(cxx_name(&member_node.name));
}
}
}
let base_text = base.map(|base| {
let path = cpp_type_path(ast, base);
let spelled: Vec<&str> = path.segments().iter().map(|part| part.as_str()).collect();
format!("::{}", spelled.join("::"))
});
members.extend(owning_members(
&name,
base_text.as_deref(),
&values,
&pointers,
));
let swappable = names.iter().all(|(spelled, _)| spelled != "swap");
if swappable {
members.push(swap_member(&name, &names));
}
let mut declared = class(&name, members, Doc::none());
if let Some(base) = base {
declared.bases.push(cpp::Base {
access: cpp::Access::Public,
virtual_: false,
path: cpp_type_path(ast, base),
});
}
out.push((node.module, cpp::Decl::Class(declared)));
if swappable {
out.push((node.module, swap_function(&name)));
}
out.extend(
idl_names(&name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
}
TypeKind::Union { discriminator, .. } => {
if ghost_created_for(applied, node.node_id, "CppUnion").is_none() {
return Ok(());
}
emitted.push(key);
let Some(disc) = discriminator.resolved() else {
return Err(MaterialiseError::Unevaluated {
construct: "union discriminator".to_string(),
at: name,
});
};
let disc_type = cpp_value_type(ast, disc, &name)?;
let mut names = Vec::new();
let mut cases: Vec<UnionCase> = Vec::new();
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, "CppCase").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(),
});
};
names.push((cxx_name(&member.name), member.name.clone()));
let mut label = None;
for candidate in &case_node.labels {
if let CaseLabel::Value(expr) = candidate {
let value = ast.eval_integral(*expr).map_err(|_| {
MaterialiseError::Unevaluated {
construct: "union case label".to_string(),
at: node.name.clone(),
}
})?;
label = Some(if value > i128::from(i64::MAX) {
format!("{value}ULL")
} else {
zerodds_lang_render::cpp::integer_text(value)
});
break;
}
}
let branch = cpp_value_type(ast, target, &member.name)?;
if super::held_externally(ast, member) {
cases.push(UnionCase {
name: cxx_name(&member.name),
type_: std_type("shared_ptr", vec![branch]),
label,
doc: external_doc(),
});
continue;
}
cases.push(UnionCase {
name: cxx_name(&member.name),
type_: branch,
label,
doc: Doc::none(),
});
}
out.push((
node.module,
cpp::Decl::Class(class(
&name,
union_members(&disc_type, &cases),
Doc::lines(&[
"A union: the accessor functions of IDL4 to C++ 1.0 (`_d()` and one",
"accessor per case) over a discriminator and one storage per branch.",
"Not a C++ `union` — a branch may be a std::string or a std::vector,",
"which a union member may be only with the lifetime managed by hand —",
"and not a std::variant, because two branches may carry the same",
"type and a variant could not then say which is live.",
"",
"Only the branch `_d()` selects is written. The others hold",
"whatever they were left holding and cost the sample their",
"storage, which is the price of not allocating.",
]),
)),
));
out.extend(
idl_names(&name, &names)?
.into_iter()
.map(|decl| (node.module, decl)),
);
}
TypeKind::Native => {
if ghost_created_for(applied, node.node_id, "CppNative").is_none() {
return Ok(());
}
emitted.push(key);
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());
out.push((
node.module,
cpp::Decl::Alias(cpp::Alias {
doc: Doc::line("A native type. Opaque unless a facet named its mapping."),
name: Ident::new(&name),
type_: match mapping {
Some(spelling) => cpp::Type::named(Path::single(&spelling)),
None => cpp::Type::Pointer(Box::new(cpp::Type::Void)),
},
}),
));
}
TypeKind::ValueBox { boxed } => {
let Some(target) = boxed.resolved() else {
return Ok(());
};
emitted.push(key);
out.push((
node.module,
cpp::Decl::Alias(cpp::Alias {
doc: Doc::line("A boxed value: a shared handle, empty when null."),
name: Ident::new(&name),
type_: std_type("shared_ptr", vec![cpp_value_type(ast, target, &name)?]),
}),
));
}
TypeKind::Primitive(_)
| TypeKind::String { .. }
| TypeKind::Fixed { .. }
| TypeKind::Sequence { .. }
| TypeKind::Array { .. }
| TypeKind::Map { .. }
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. }
| TypeKind::Any
| TypeKind::ValueBase => return Ok(()),
}
if has_idl_name(&node.kind) && !node.name.is_empty() {
out.push((node.module, idl_name(&name, &node.name)));
}
Ok(())
}
fn enum_underlying(bound: u16, values: &[(String, i128)]) -> cpp::Type {
let signed = values.iter().any(|(_, value)| *value < 0);
let mut bits = bound.max(1);
for (_, value) in values {
bits = bits.max(bits_for(*value, signed));
}
if signed {
return match bits {
0..=8 => std_type("int8_t", Vec::new()),
9..=16 => std_type("int16_t", Vec::new()),
17..=32 => std_type("int32_t", Vec::new()),
_ => std_type("int64_t", Vec::new()),
};
}
cpp_holder(bits)
}
fn bits_for(value: i128, signed: bool) -> u16 {
for bits in [8u16, 16, 32] {
let fits = if signed {
let limit = 1i128 << (bits - 1);
value >= -limit && value < limit
} else {
value >= 0 && value < (1i128 << bits)
};
if fits {
return bits;
}
}
64
}
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))
})
}
fn cpp_constant(
value: &zerodds_ast::ConstValue,
declared: cpp::Type,
at: &str,
) -> Result<(cpp::Type, Literal), MaterialiseError> {
use zerodds_ast::ConstValue;
let refuse = |construct: &str| MaterialiseError::NoLanguageForm {
construct: construct.to_string(),
language: "C++",
at: at.to_string(),
};
fn text_type() -> cpp::Type {
cpp::Type::Pointer(Box::new(cpp::Type::Const(Box::new(cpp::Type::Primitive(
cpp::Primitive::Char,
)))))
}
Ok(match value {
ConstValue::Boolean(flag) => (declared, Literal::Bool(*flag)),
ConstValue::Integer(number) => (declared, int_literal(*number)),
ConstValue::Char(character) => (declared, Literal::Char(*character)),
ConstValue::WChar(point) => (declared, int_literal(i128::from(*point))),
ConstValue::Float { mantissa, exponent } => {
if matches!(&declared, cpp::Type::Named { path, .. } if path
.segments()
.last()
.is_some_and(|last| last.as_str() == "Float128"))
{
return Err(refuse("a `long double` constant, which has no C++ literal"));
}
let mut text = constant::float_text(mantissa, exponent.as_deref());
if matches!(&declared, cpp::Type::Primitive(cpp::Primitive::Float)) {
text.push('f');
}
(declared, Literal::Float(text))
}
ConstValue::Fixed { digits, scale } => (
text_type(),
Literal::Str(constant::fixed_decimal(digits, *scale)),
),
ConstValue::String(text) => (text_type(), Literal::Str(text.clone())),
ConstValue::WString(points) => (
text_type(),
Literal::Str(
constant::wstring_utf8(points)
.ok_or_else(|| refuse("a `wstring` constant holding a lone surrogate"))?,
),
),
_ => return Err(refuse("a constant form this binding has no literal for")),
})
}
#[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 corpus() -> PathBuf {
FsPath::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("tests")
.join("interop-corpus")
}
fn model(source: &str, path: &str) -> Result<Ast, String> {
let parsed = zerodds_idl_parse::parse(source);
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;
let report = ast
.resolve_all_by_name()
.map_err(|error| error.to_string())?;
if !report.is_clean() {
return Err(format!("resolve: {:?}", report.broken));
}
Ok(ast)
}
fn header(source: &str, path: &str) -> Result<String, String> {
let ast = model(source, path)?;
let graph = ModelGraph::of(&ast);
let applied = apply(&graph, &rule::cpp::catalogue()).map_err(|error| error.to_string())?;
let decls = cpp_items(&ast, &applied).map_err(|error| error.to_string())?;
Ok(zerodds_lang_render::cpp::file(&cpp::File {
doc: Doc::line("Generated by zerodds-lang-rules. Do not edit."),
pragma_once: true,
includes: includes(),
decls,
}))
}
#[test]
fn every_corpus_case_renders_or_names_its_refusal() {
let dump = std::env::var("ZERODDS_CPP_OUT").ok();
if let Some(directory) = &dump {
std::fs::create_dir_all(directory).expect("the dump directory");
}
let mut cases: Vec<PathBuf> = std::fs::read_dir(corpus())
.expect("the interop corpus")
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.join("type.idl").is_file())
.collect();
cases.sort();
assert!(
cases.len() > 300,
"the corpus has {} cases, which is not it",
cases.len()
);
let mut rendered = 0usize;
let mut refused: Vec<String> = Vec::new();
for case in &cases {
let name = case
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("?")
.to_string();
let idl = case.join("type.idl");
let source = std::fs::read_to_string(&idl).expect("the case's IDL");
match header(&source, &idl.display().to_string()) {
Ok(text) => {
assert!(!text.is_empty(), "{name} rendered nothing");
if let Some(directory) = &dump {
std::fs::write(FsPath::new(directory).join(format!("{name}.hpp")), &text)
.expect("the dumped header");
}
rendered += 1;
}
Err(message) => refused.push(format!("{name}: {message}")),
}
}
assert_eq!(
rendered + refused.len(),
cases.len(),
"every case is either rendered or refused"
);
let refused = crate::adapter::without_the_front_end_refusals(refused);
assert!(
refused.is_empty(),
"{} of {} cases were refused:\n{}",
refused.len(),
cases.len(),
refused.join("\n")
);
}
#[test]
fn an_optional_member_is_a_plain_pointer_and_not_a_second_field() {
let text = header(
"@appendable struct S { @optional long a; long b; };",
"t.idl",
)
.expect("renders");
assert!(text.contains("std::int32_t* a = nullptr;"), "{text}");
assert!(!text.contains("std::optional"), "{text}");
assert!(!text.contains("_has_a"), "{text}");
assert!(text.contains("~S()"), "{text}");
assert!(text.contains("S(const S& zd_other)"), "{text}");
assert!(text.contains("zerodds::wire::clone(zd_other.a)"), "{text}");
assert!(text.contains("zerodds::wire::destroy(a);"), "{text}");
}
#[test]
fn a_module_is_a_namespace_and_not_a_prefix() {
let text =
header("module a { module b { struct P { long x; }; }; };", "t.idl").expect("renders");
assert!(text.contains("namespace a {"), "{text}");
assert!(text.contains("namespace b {"), "{text}");
assert!(!text.contains("a_b_P"), "{text}");
}
#[test]
fn a_reference_across_modules_is_written_from_the_root() {
let text = header(
"module a { struct P { long x; }; }; module b { struct Q { a::P p; }; };",
"t.idl",
)
.expect("renders");
assert!(text.contains("::a::P p;"), "{text}");
}
#[test]
fn a_sequence_is_a_vector_and_a_map_is_a_class_over_a_vector_of_pairs() {
let text = header(
"struct S { sequence<long> items; map<long, double> m; };",
"t.idl",
)
.expect("renders");
assert!(text.contains("std::vector<std::int32_t> items;"), "{text}");
assert!(
text.contains("::zerodds::Map<std::int32_t, double> m;"),
"{text}"
);
assert!(!text.contains("std::map<"), "{text}");
}
#[test]
fn a_structure_has_swap_as_a_member_and_in_its_namespace() {
let text = header("struct S { long a; long b; };", "t.idl").expect("renders");
assert!(text.contains("zd_other.a"), "{text}");
assert!(text.contains("zd_other.b"), "{text}");
assert!(text.contains("zd_a.swap(zd_b)"), "{text}");
let colliding = header("struct T { long swap; };", "t.idl").expect("renders");
assert!(!colliding.contains("zd_other"), "{colliding}");
}
#[test]
fn an_array_nests_with_the_outer_dimension_outermost() {
let text = header("struct S { long a[2][3]; };", "t.idl").expect("renders");
assert!(
text.contains("std::array<std::array<std::int32_t, 3>, 2> a = {};"),
"{text}"
);
}
#[test]
fn a_union_is_a_class_with_a_discriminator_and_one_field_per_case() {
let text = header(
"union U switch (long) { case 1: long a; case 2: long b; };",
"t.idl",
)
.expect("renders");
assert!(text.contains("struct U {"), "{text}");
assert!(text.contains("std::int32_t _d() const"), "{text}");
assert!(text.contains("void _d(std::int32_t value)"), "{text}");
assert!(text.contains("std::int32_t zd_d;"), "{text}");
assert!(text.contains("const std::int32_t& a() const"), "{text}");
assert!(text.contains("std::int32_t& a()"), "{text}");
assert!(text.contains("void a(const std::int32_t& value)"), "{text}");
assert!(text.contains("void a(std::int32_t&& value)"), "{text}");
assert!(
text.contains("zd_a = value; zd_d = static_cast<std::int32_t>(1);"),
"{text}"
);
assert!(
text.contains("zd_b = value; zd_d = static_cast<std::int32_t>(2);"),
"{text}"
);
assert!(text.contains("std::int32_t zd_a;"), "{text}");
assert!(text.contains("std::int32_t zd_b;"), "{text}");
assert!(!text.contains("union U"), "{text}");
assert!(!text.contains("std::variant<"), "{text}");
}
#[test]
fn an_enumeration_takes_an_underlying_type_from_its_bit_bound() {
let narrow = header("@bit_bound(8) enum E { A, B };", "t.idl").expect("renders");
assert!(narrow.contains("enum class E : std::uint8_t {"), "{narrow}");
let wide = header("enum W { A, B };", "t.idl").expect("renders");
assert!(wide.contains("enum class W : std::uint32_t {"), "{wide}");
}
#[test]
fn a_long_double_is_the_runtime_type_and_never_the_compiler_s() {
let text = header("struct S { long double d; double e; };", "t.idl").expect("renders");
assert!(text.contains("::zerodds::wire::Float128 d = {};"), "{text}");
assert!(text.contains("double e = {};"), "{text}");
assert!(!text.contains("long double"), "{text}");
}
#[test]
fn the_tag_keeps_apart_what_the_cpp_spelling_collapses() {
let ast = model(
"struct S { octet a; uint8 b; double c; long double d; string e; wstring f; };",
"t.idl",
)
.expect("model");
let mut tags = Vec::new();
let mut spellings = Vec::new();
let members = ast
.types()
.find_map(|(id, node)| {
matches!(node.kind, TypeKind::Structure { .. }).then(|| ast.all_members_of(id))
})
.expect("the structure");
for member in members {
let node = ast.member(member).expect("member");
let target = node.type_.resolved().expect("resolved");
tags.push(cpp_type_tag(&ast, target));
spellings.push(zerodds_lang_render::cpp::type_(
&cpp_value_type(&ast, target, "t").expect("type"),
));
}
assert_eq!(tags.len(), 6);
let mut unique = tags.clone();
unique.sort();
unique.dedup();
assert_eq!(unique.len(), 6, "the tags collapse: {tags:?}");
let mut collapsed = spellings.clone();
collapsed.sort();
collapsed.dedup();
assert!(
collapsed.len() < 6,
"the spellings do not collapse, so the tag would be redundant: {spellings:?}"
);
}
#[test]
fn the_integers_are_spelled_with_the_c99_types_of_table_43() {
let text = header(
"struct S { int8 a; uint8 b; short c; unsigned short d; long e; unsigned long f; \
long long g; unsigned long long h; };",
"t.idl",
)
.expect("renders");
for expected in [
"std::int8_t a ",
"std::uint8_t b ",
"std::int16_t c ",
"std::uint16_t d ",
"std::int32_t e ",
"std::uint32_t f ",
"std::int64_t g ",
"std::uint64_t h ",
] {
assert!(text.contains(expected), "{expected}:\n{text}");
}
}
#[test]
fn the_octet_is_not_the_type_of_uint8_or_int8_or_char() {
let text = header(
"struct S { octet a; uint8 b; int8 c; char d; boolean e; };",
"t.idl",
)
.expect("renders");
assert!(text.contains("::zerodds::wire::Octet a "), "{text}");
assert!(text.contains("std::uint8_t b "), "{text}");
assert!(text.contains("std::int8_t c "), "{text}");
assert!(text.contains("char d "), "{text}");
assert!(text.contains("bool e "), "{text}");
}
#[test]
fn an_any_member_is_refused_by_name_at_any_depth() {
let bare = header("struct S { any a; };", "t.idl").expect_err("refused");
assert!(bare.contains("C++ has no form for `any`"), "{bare}");
let nested = header("struct S { sequence<any> a; };", "t.idl").expect_err("refused");
assert!(nested.contains("C++ has no form for `any`"), "{nested}");
}
}