use crate::contract::{self, Consumed};
use crate::encode::ProbeError;
use crate::target_keywords::c;
use serde_json::Value;
use std::borrow::Cow;
use std::fmt::Write as _;
use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vendor {
Cyclone,
Rti,
}
impl Vendor {
pub const ALL: [Self; 2] = [Self::Cyclone, Self::Rti];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Cyclone => "cyclone",
Self::Rti => "rti",
}
}
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
Self::ALL.into_iter().find(|v| v.as_str() == value)
}
}
#[derive(Debug, Default)]
pub struct Fill {
declarations: String,
statements: String,
next: usize,
seen: Consumed,
}
impl Fill {
#[must_use]
pub fn into_source(self) -> String {
let mut out = self.declarations;
out.push_str(&self.statements);
out
}
fn temporary(&mut self, stem: &str) -> String {
self.next += 1;
format!("zd_{stem}_{}", self.next)
}
}
pub fn fill(
ast: &Ast,
type_: Id<TypeNode>,
values: &Value,
vendor: Vendor,
) -> Result<String, ProbeError> {
fill_recording(ast, type_, values, vendor, &mut Consumed::new())
}
pub fn fill_recording(
ast: &Ast,
type_: Id<TypeNode>,
values: &Value,
vendor: Vendor,
seen: &mut Consumed,
) -> Result<String, ProbeError> {
contract::reject_alias_cycle(ast)?;
let mut out = Fill::default();
let root = contract::root_path(ast, type_);
let result = emit(ast, type_, values, vendor, "sample", &mut out, &root);
*seen = core::mem::take(&mut out.seen);
result?;
Ok(out.into_source())
}
#[must_use]
pub fn c_name(ast: &Ast, type_: Id<TypeNode>, vendor: Vendor) -> String {
ast.any_of_type(type_)
.ok()
.and_then(|any| ast.scoped_name_of(any))
.map_or_else(
|| String::from("UNKNOWN"),
|scoped| identifier(vendor, &scoped.segments().join("_")).into_owned(),
)
}
#[allow(clippy::too_many_lines)]
fn emit(
ast: &Ast,
type_: Id<TypeNode>,
value: &Value,
vendor: Vendor,
lvalue: &str,
out: &mut Fill,
path: &str,
) -> Result<(), ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
match &node.kind {
TypeKind::Primitive(PrimitiveKind::Float128) => Err(ProbeError::Unsupported {
path: path.to_string(),
kind: match vendor {
Vendor::Cyclone => "a long double, which Cyclone's idlc refuses to generate",
Vendor::Rti => "a long double, which RTI leaves as sixteen opaque octets",
},
}),
TypeKind::Primitive(kind) => {
let literal = primitive_literal(*kind, value, path)?;
out.seen.scalar(path);
let _ = writeln!(out.statements, "{lvalue} = {literal};");
Ok(())
}
TypeKind::String {
width: CharWidth::Char,
bound,
} => {
let text = contract::text(value, path)?;
out.seen.scalar(path);
let literal = c_string(text);
match vendor {
Vendor::Cyclone => match bound_count(ast, *bound, path)? {
Some(n) => {
let _ = writeln!(
out.statements,
"strncpy({lvalue}, {literal}, {n}); {lvalue}[{n}] = '\\0';"
);
}
None => {
let _ = writeln!(out.statements, "{lvalue} = (char *){literal};");
}
},
Vendor::Rti => {
let _ = writeln!(out.statements, "{lvalue} = DDS_String_dup({literal});");
}
}
Ok(())
}
TypeKind::String {
width: CharWidth::WChar,
bound,
} => {
let text = contract::text(value, path)?;
out.seen.scalar(path);
let bounded = vendor == Vendor::Cyclone && bound_count(ast, *bound, path)?.is_some();
emit_wstring(text, lvalue, bounded, out);
Ok(())
}
TypeKind::Bitmask { .. } => {
let set = contract::bit_names(ast, type_, value, path)?;
emit_bitmask(ast, type_, &set, vendor, lvalue, out, path);
Ok(())
}
TypeKind::Enumeration { .. } => {
let (name, _) = contract::enumerator(ast, type_, value, path)?;
out.seen.scalar(path);
let prefix = match vendor {
Vendor::Cyclone => module_prefix(ast, type_, vendor),
Vendor::Rti => String::new(),
};
let _ = writeln!(
out.statements,
"{lvalue} = {prefix}{};",
identifier(vendor, &name)
);
Ok(())
}
TypeKind::Alias { aliased } => {
let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
})?;
emit(ast, target, value, vendor, lvalue, out, path)
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
let object = contract::object(value, path)?;
let c_fields = crate::dump::c_members(ast, type_, lvalue, vendor);
let declared: Vec<String> = c_fields
.iter()
.map(|(member, _, _)| member.name.clone())
.collect();
contract::reject_unknown_keys(object, &declared, path)?;
for (member, field, external) in &c_fields {
let Some(member_value) = contract::member_value(object, member, path)? else {
continue;
};
let member_path = contract::join(path, &member.name);
let plain_pointer_string =
is_string_type(ast, member.type_) && (vendor == Vendor::Cyclone || !*external);
if (member.optional || *external) && !plain_pointer_string {
let backing = out.temporary("opt");
let _ = writeln!(out.declarations, "static __typeof__(*{field}) {backing};");
emit(
ast,
member.type_,
member_value,
vendor,
&backing,
out,
&member_path,
)?;
let _ = writeln!(out.statements, "{field} = &{backing};");
continue;
}
emit(
ast,
member.type_,
member_value,
vendor,
field,
out,
&member_path,
)?;
}
Ok(())
}
TypeKind::Union { .. } => {
let object = contract::object(value, path)?;
let discriminant = contract::discriminator(object, path)?;
for (name, member_value) in object {
if name == "_d" {
continue;
}
let Some((member_type, _)) = union_member(ast, type_, name) else {
return Err(ProbeError::Unknown {
path: contract::join(path, name),
declared: union_member_names(ast, type_),
});
};
contract::reject_null(member_value, &contract::join(path, name))?;
emit(
ast,
member_type,
member_value,
vendor,
&format!("{lvalue}._u.{}", identifier(vendor, name)),
out,
&contract::join(path, name),
)?;
}
let TypeKind::Union { discriminator, .. } = &node.kind else {
return Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a union whose kind changed under the emitter",
});
};
let discriminator = discriminator
.resolved()
.ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the discriminator is unresolved"),
})?;
emit(
ast,
discriminator,
discriminant,
vendor,
&format!("{lvalue}._d"),
out,
&contract::join(path, "_d"),
)
}
TypeKind::Array { element, dims } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the array element is unresolved"),
})?;
let counts = contract::array_dimensions(ast, dims, path)?;
emit_array(ast, element, value, vendor, lvalue, out, path, &counts)
}
TypeKind::Sequence { element, bound } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the sequence element is unresolved"),
})?;
let items = contract::bounded_items(ast, value, *bound, "elements", path)?;
emit_sequence(ast, element, items, vendor, lvalue, out, path)
}
TypeKind::Bitset { .. } => Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a bitset, for which XTypes 1.3 §7.5.1 defines no C binding",
}),
TypeKind::Map { .. } => Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a map, which the IDL-to-C mapping §7.5.1 reuses does not have",
}),
TypeKind::Fixed { .. } => Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a fixed, which the IDL-to-C mapping §7.5.1 reuses does not have",
}),
other => Err(ProbeError::Unfilled {
path: path.to_string(),
kind: other.as_str(),
}),
}
}
fn emit_wstring(text: &str, lvalue: &str, bounded: bool, out: &mut Fill) {
let buffer = out.temporary("wstr");
let mut units = String::new();
for unit in text.encode_utf16() {
let _ = write!(units, "0x{unit:04x}, ");
}
let _ = writeln!(
out.declarations,
"static __typeof__(*{lvalue}) {buffer}[] = {{ {units}0x0000 }};"
);
if bounded {
let _ = writeln!(
out.statements,
"memcpy({lvalue}, {buffer}, sizeof({buffer}));"
);
} else {
let _ = writeln!(out.statements, "{lvalue} = {buffer};");
}
}
fn bound_count(
ast: &Ast,
bound: zerodds_ast::Bound,
path: &str,
) -> Result<Option<i128>, ProbeError> {
match bound.expr() {
None => Ok(None),
Some(expr) => ast
.eval_integral(expr)
.map(Some)
.map_err(|_| ProbeError::NoLayout {
detail: format!("{path}: a bound that does not evaluate to a count"),
}),
}
}
#[allow(clippy::too_many_arguments)]
fn emit_bitmask(
ast: &Ast,
type_: Id<TypeNode>,
set: &[(String, u16)],
vendor: Vendor,
lvalue: &str,
out: &mut Fill,
path: &str,
) {
let prefix = match vendor {
Vendor::Cyclone => module_prefix(ast, type_, vendor),
Vendor::Rti => String::new(),
};
let mut flags = Vec::with_capacity(set.len());
for (index, (name, _)) in set.iter().enumerate() {
out.seen.scalar(&format!("{path}[{index}]"));
flags.push(format!("{prefix}{}", identifier(vendor, name)));
}
let value = if flags.is_empty() {
"0".to_string()
} else {
flags.join(" | ")
};
let _ = writeln!(out.statements, "{lvalue} = {value};");
}
#[allow(clippy::too_many_arguments)]
fn emit_array(
ast: &Ast,
element: Id<TypeNode>,
value: &Value,
vendor: Vendor,
lvalue: &str,
out: &mut Fill,
path: &str,
counts: &[usize],
) -> Result<(), ProbeError> {
let Some((&count, rest)) = counts.split_first() else {
return emit(ast, element, value, vendor, lvalue, out, path);
};
let items = contract::array(value, path)?;
if items.len() != count {
return Err(ProbeError::Mismatch {
path: path.to_string(),
expected: format!("exactly {count} elements"),
found: format!("{}", items.len()),
});
}
for (index, item) in items.iter().enumerate() {
emit_array(
ast,
element,
item,
vendor,
&format!("{lvalue}[{index}]"),
out,
&format!("{path}[{index}]"),
rest,
)?;
}
Ok(())
}
fn emit_sequence(
ast: &Ast,
element: Id<TypeNode>,
items: &[Value],
vendor: Vendor,
lvalue: &str,
out: &mut Fill,
path: &str,
) -> Result<(), ProbeError> {
let count = items.len();
match vendor {
Vendor::Cyclone => {
let buffer = out.temporary("seq");
let element_c = if is_wide_string(ast, element) {
format!("__typeof__(*{lvalue}._buffer)")
} else {
c_type_name(ast, element, vendor)
};
if count == 0 {
let _ = writeln!(
out.statements,
"{lvalue}._length = 0; {lvalue}._maximum = 0; \
{lvalue}._buffer = NULL; {lvalue}._release = false;"
);
return Ok(());
}
let _ = writeln!(out.declarations, "static {element_c} {buffer}[{count}];");
for (index, item) in items.iter().enumerate() {
emit(
ast,
element,
item,
vendor,
&format!("{buffer}[{index}]"),
out,
&format!("{path}[{index}]"),
)?;
}
let _ = writeln!(
out.statements,
"{lvalue}._length = {count}; {lvalue}._maximum = {count}; \
{lvalue}._buffer = {buffer}; {lvalue}._release = false;"
);
Ok(())
}
Vendor::Rti => {
let seq = rti_sequence_name(ast, element, vendor);
let _ = writeln!(
out.statements,
"{seq}_set_maximum(&{lvalue}, {count});\n\
{seq}_set_length(&{lvalue}, {count});"
);
for (index, item) in items.iter().enumerate() {
let target = format!("(*{seq}_get_reference(&{lvalue}, {index}))");
emit(
ast,
element,
item,
vendor,
&target,
out,
&format!("{path}[{index}]"),
)?;
}
Ok(())
}
}
}
fn is_wide_string(ast: &Ast, type_: Id<TypeNode>) -> bool {
match ast.type_(type_).map(|node| node.kind.clone()) {
Ok(TypeKind::String { width, .. }) => width == CharWidth::WChar,
Ok(TypeKind::Alias { aliased }) => aliased
.resolved()
.is_some_and(|target| is_wide_string(ast, target)),
_ => false,
}
}
fn c_type_name(ast: &Ast, type_: Id<TypeNode>, vendor: Vendor) -> String {
match ast.type_(type_).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(kind)) => c_primitive(kind).to_string(),
Ok(TypeKind::String { .. }) => "char *".to_string(),
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
None => "void".to_string(),
Some(target) => {
let collection = matches!(
ast.type_(target).map(|node| node.kind.clone()),
Ok(TypeKind::Sequence { .. } | TypeKind::Array { .. })
);
if collection && vendor == Vendor::Cyclone {
c_name(ast, type_, vendor)
} else {
c_type_name(ast, target, vendor)
}
}
},
Ok(TypeKind::Sequence { element, .. }) if vendor == Vendor::Cyclone => element
.resolved()
.map_or_else(|| "void".to_string(), |e| cyclone_sequence_name(ast, e)),
_ => c_name(ast, type_, vendor),
}
}
pub(crate) fn is_string_type(ast: &Ast, type_: Id<TypeNode>) -> bool {
let resolved = contract::resolve_alias(ast, type_);
matches!(
ast.type_(resolved).map(|node| node.kind.clone()),
Ok(TypeKind::String { .. })
)
}
fn cyclone_sequence_name(ast: &Ast, element: Id<TypeNode>) -> String {
format!("dds_sequence_{}", cyclone_sequence_tag(ast, element))
}
fn cyclone_sequence_tag(ast: &Ast, element: Id<TypeNode>) -> String {
match ast.type_(element).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(kind)) => cyclone_primitive_tag(kind).to_string(),
Ok(TypeKind::Sequence { element: inner, .. }) => inner.resolved().map_or_else(
|| "void".to_string(),
|e| format!("sequence_{}", cyclone_sequence_name(ast, e)),
),
_ => c_name(ast, element, Vendor::Cyclone),
}
}
const fn cyclone_primitive_tag(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "bool",
PrimitiveKind::Octet => "octet",
PrimitiveKind::Char => "char",
PrimitiveKind::WChar => "wchar",
PrimitiveKind::Int8 => "int8",
PrimitiveKind::UInt8 => "uint8",
PrimitiveKind::Int16 => "short",
PrimitiveKind::UInt16 => "unsigned_short",
PrimitiveKind::Int32 => "long",
PrimitiveKind::UInt32 => "unsigned_long",
PrimitiveKind::Int64 => "long_long",
PrimitiveKind::UInt64 => "unsigned_long_long",
PrimitiveKind::Float32 => "float",
PrimitiveKind::Float64 => "double",
PrimitiveKind::Float128 => "long_double",
}
}
fn rti_sequence_name(ast: &Ast, element: Id<TypeNode>, vendor: Vendor) -> String {
match ast.type_(element).map(|node| node.kind.clone()) {
Ok(TypeKind::Primitive(PrimitiveKind::UInt8)) => "DDS_UInt8Seq".to_string(),
Ok(TypeKind::Primitive(PrimitiveKind::Int8)) => "DDS_Int8Seq".to_string(),
Ok(TypeKind::Primitive(kind)) => format!("DDS_{}Seq", rti_primitive(kind)),
Ok(TypeKind::String {
width: CharWidth::WChar,
..
}) => "DDS_WstringSeq".to_string(),
Ok(TypeKind::String { .. }) => "DDS_StringSeq".to_string(),
Ok(TypeKind::Alias { aliased }) => match aliased.resolved() {
None => "DDS_LongSeq".to_string(),
Some(target) => {
let collection = matches!(
ast.type_(target).map(|node| node.kind.clone()),
Ok(TypeKind::Sequence { .. } | TypeKind::Array { .. })
);
if collection {
format!("{}Seq", c_name(ast, element, vendor))
} else {
rti_sequence_name(ast, target, vendor)
}
}
},
_ => format!("{}Seq", c_name(ast, element, vendor)),
}
}
const fn c_primitive(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "bool",
PrimitiveKind::Octet | PrimitiveKind::UInt8 => "uint8_t",
PrimitiveKind::Int8 => "int8_t",
PrimitiveKind::Char => "char",
PrimitiveKind::WChar => "wchar_t",
PrimitiveKind::Int16 => "int16_t",
PrimitiveKind::UInt16 => "uint16_t",
PrimitiveKind::Int32 => "int32_t",
PrimitiveKind::UInt32 => "uint32_t",
PrimitiveKind::Int64 => "int64_t",
PrimitiveKind::UInt64 => "uint64_t",
PrimitiveKind::Float32 => "float",
PrimitiveKind::Float64 | PrimitiveKind::Float128 => "double",
}
}
const fn rti_primitive(kind: PrimitiveKind) -> &'static str {
match kind {
PrimitiveKind::Boolean => "Boolean",
PrimitiveKind::Octet | PrimitiveKind::UInt8 => "Octet",
PrimitiveKind::Int8 => "Char",
PrimitiveKind::Char => "Char",
PrimitiveKind::WChar => "Wchar",
PrimitiveKind::Int16 => "Short",
PrimitiveKind::UInt16 => "UnsignedShort",
PrimitiveKind::Int32 => "Long",
PrimitiveKind::UInt32 => "UnsignedLong",
PrimitiveKind::Int64 => "LongLong",
PrimitiveKind::UInt64 => "UnsignedLongLong",
PrimitiveKind::Float32 => "Float",
PrimitiveKind::Float64 | PrimitiveKind::Float128 => "Double",
}
}
fn module_prefix(ast: &Ast, type_: Id<TypeNode>, vendor: Vendor) -> String {
let Some(scoped) = ast
.any_of_type(type_)
.ok()
.and_then(|any| ast.scoped_name_of(any))
else {
return String::new();
};
let segments = scoped.segments();
if segments.len() <= 1 {
return String::new();
}
let mut prefix = segments[..segments.len() - 1]
.iter()
.map(|segment| identifier(vendor, segment).into_owned())
.collect::<Vec<String>>()
.join("_");
prefix.push('_');
prefix
}
pub(crate) fn identifier(vendor: Vendor, name: &str) -> Cow<'_, str> {
if zerodds_idl_cst::keyword_collision(name).is_some()
|| zerodds_idl_cst::KEYWORDS
.iter()
.any(|(keyword, _)| *keyword == name)
{
return match vendor {
Vendor::Cyclone => Cow::Borrowed(name),
Vendor::Rti => Cow::Owned(format!("_{name}")),
};
}
match vendor {
Vendor::Cyclone => Cow::Borrowed(name),
Vendor::Rti => c(name),
}
}
fn union_member(ast: &Ast, union: Id<TypeNode>, name: &str) -> Option<(Id<TypeNode>, String)> {
ast.cases_of(union)
.iter()
.filter_map(|id| ast.case(*id).ok())
.filter_map(|case| ast.member(case.member).ok())
.find(|member| member.name == name)
.and_then(|member| Some((member.type_.resolved()?, member.name.clone())))
}
fn union_member_names(ast: &Ast, union: Id<TypeNode>) -> Vec<String> {
ast.cases_of(union)
.iter()
.filter_map(|id| ast.case(*id).ok())
.filter_map(|case| ast.member(case.member).ok())
.map(|member| member.name.clone())
.collect()
}
fn primitive_literal(kind: PrimitiveKind, value: &Value, path: &str) -> Result<String, ProbeError> {
Ok(match kind {
PrimitiveKind::Boolean => {
if contract::boolean(value, path)? {
"true".to_string()
} else {
"false".to_string()
}
}
PrimitiveKind::Char => {
format!(
"0x{:02x}",
contract::character_for(kind, value, path)? as u32
)
}
PrimitiveKind::WChar => {
format!(
"0x{:04x}",
contract::character_for(kind, value, path)? as u32
)
}
PrimitiveKind::Float32 => {
format!("{:.17e}f", contract::float(value, path)?)
}
PrimitiveKind::Float64 | PrimitiveKind::Float128 => {
format!("{:.17e}", contract::float(value, path)?)
}
PrimitiveKind::Int64 => {
let n = contract::integer_for(kind, value, path)?;
if n == i128::from(i64::MIN) {
"(-9223372036854775807LL - 1)".to_string()
} else {
format!("{n}LL")
}
}
PrimitiveKind::UInt64 => {
format!("{}ULL", contract::integer_for(kind, value, path)?)
}
_ => format!("{}", contract::integer_for(kind, value, path)?),
})
}
fn c_string(text: &str) -> String {
let mut out = String::from("\"");
for byte in text.as_bytes() {
match byte {
b'"' => out.push_str("\\\""),
b'\\' => out.push_str("\\\\"),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0x20..=0x7E => out.push(char::from(*byte)),
other => {
let _ = write!(out, "\\{other:03o}");
}
}
}
out.push('"');
out
}