use crate::contract;
use crate::encode::ProbeError;
use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
#[derive(Debug, Default)]
struct Dump {
body: String,
depth: usize,
rec: Vec<Id<TypeNode>>,
}
impl Dump {
fn line(&mut self, text: &str) {
for _ in 0..self.depth {
self.body.push_str(" ");
}
self.body.push_str(text);
self.body.push('\n');
}
fn open(&mut self, text: &str) {
self.line(text);
self.depth += 1;
}
fn close(&mut self, text: &str) {
self.depth = self.depth.saturating_sub(1);
self.line(text);
}
fn raw(&mut self, literal: &str) {
let escaped = literal.replace('\\', "\\\\").replace('"', "\\\"");
self.line(&format!("zdj_raw(\"{escaped}\");"));
}
}
pub fn dump_ros(ast: &Ast, type_: Id<TypeNode>) -> Result<String, ProbeError> {
contract::reject_alias_cycle(ast)?;
let mut out = Dump {
rec: crate::recursion::recursive_aggregates(ast, type_),
..Dump::default()
};
emit(ast, type_, "sample", &mut out, "")?;
out.line("zdj_raw(\"\\n\");");
Ok(out.body)
}
fn emit(
ast: &Ast,
type_: Id<TypeNode>,
expr: &str,
out: &mut Dump,
path: &str,
) -> Result<(), ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
match &node.kind {
TypeKind::Primitive(kind) => {
out.line(&primitive_call(*kind, expr));
Ok(())
}
TypeKind::String {
width: CharWidth::Char,
..
} => {
out.line(&format!("ZDROS_STRING_OUT(&{expr});"));
Ok(())
}
TypeKind::String {
width: CharWidth::WChar,
..
} => {
out.line(&format!(
"zdj_wstring((const unsigned short *){expr}.data, {expr}.size);"
));
Ok(())
}
TypeKind::Alias { aliased } => {
let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
})?;
emit(ast, target, expr, out, path)
}
TypeKind::Structure { .. } => {
if out.rec.contains(&type_) {
return Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a recursive type, for which §7.4.1.1 gives no encoding to compare \
against — the same refusal `ros_idl` makes, said here because the \
expansion of a cycle does not end",
});
}
emit_aggregate(ast, type_, expr, out, path)
}
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, expr, out, path, &counts)
}
TypeKind::Sequence { element, .. } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the sequence element is unresolved"),
})?;
emit_sequence(ast, element, expr, out, path)
}
other => Err(ProbeError::Unfilled {
path: path.to_string(),
kind: other.as_str(),
}),
}
}
fn emit_aggregate(
ast: &Ast,
type_: Id<TypeNode>,
expr: &str,
out: &mut Dump,
path: &str,
) -> Result<(), ProbeError> {
out.open("{");
out.line("int zd_first = 1;");
out.raw("{");
for member in contract::members(ast, type_) {
let member_path = contract::join(path, &member.name);
out.line(&format!("zdj_key(&zd_first, \"{}\");", member.name));
emit(
ast,
member.type_,
&format!("{expr}.{}", member.name),
out,
&member_path,
)?;
}
out.raw("}");
out.close("}");
Ok(())
}
fn emit_array(
ast: &Ast,
element: Id<TypeNode>,
expr: &str,
out: &mut Dump,
path: &str,
counts: &[usize],
) -> Result<(), ProbeError> {
let Some((&count, rest)) = counts.split_first() else {
return emit(ast, element, expr, out, path);
};
out.raw("[");
for index in 0..count {
if index > 0 {
out.raw(",");
}
emit_array(
ast,
element,
&format!("{expr}[{index}]"),
out,
&format!("{path}[{index}]"),
rest,
)?;
}
out.raw("]");
Ok(())
}
fn emit_sequence(
ast: &Ast,
element: Id<TypeNode>,
expr: &str,
out: &mut Dump,
path: &str,
) -> Result<(), ProbeError> {
out.open("{");
out.line(&format!("size_t zd_n = {expr}.size;"));
out.raw("[");
out.open("for (size_t zd_i = 0; zd_i < zd_n; zd_i++) {");
out.line("if (zd_i > 0) { zdj_raw(\",\"); }");
emit(
ast,
element,
&format!("{expr}.data[zd_i]"),
out,
&format!("{path}[]"),
)?;
out.close("}");
out.raw("]");
out.close("}");
Ok(())
}
fn primitive_call(kind: PrimitiveKind, expr: &str) -> String {
match kind {
PrimitiveKind::Boolean => format!("zdj_bool(({expr}) ? 1 : 0);"),
PrimitiveKind::Char => format!("zdj_char((char)(unsigned char)({expr}));"),
PrimitiveKind::WChar => format!("zdj_wchar((unsigned long)({expr}));"),
PrimitiveKind::Float32 => format!("zdj_f32((float)({expr}));"),
PrimitiveKind::Float64 => format!("zdj_f64((double)({expr}));"),
PrimitiveKind::Float128 => format!("zdj_f64((double)({expr}));"),
PrimitiveKind::Int64 => format!("zdj_i64_string((long long)({expr}));"),
PrimitiveKind::UInt64 => format!("zdj_u64_string((unsigned long long)({expr}));"),
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::UInt16
| PrimitiveKind::UInt32 => format!("zdj_u64((unsigned long long)({expr}));"),
PrimitiveKind::Int8 | PrimitiveKind::Int16 | PrimitiveKind::Int32 => {
format!("zdj_i64((long long)({expr}));")
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::dump_ros;
use crate::encode::model_from_idl;
fn emitted(idl: &str, type_name: &str) -> String {
let ast = model_from_idl(idl, "dump_ros.idl").expect("the fixture parses");
let type_ = ast
.find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(vec![
type_name.to_string(),
]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.expect("the type is in the model");
dump_ros(&ast, type_).expect("the type has a rosidl shape")
}
#[test]
fn a_structure_prints_its_members_in_declaration_order() {
let c = emitted("@final struct S { long a; double b; };", "S");
let a = c.find("\\\"a\\\"").or_else(|| c.find("\"a\""));
let b = c.find("\\\"b\\\"").or_else(|| c.find("\"b\""));
assert!(a < b, "{c}");
assert!(c.contains("zdj_i64((long long)(sample.a));"), "{c}");
assert!(c.contains("zdj_f64((double)(sample.b));"), "{c}");
}
#[test]
fn a_string_is_printed_through_the_guard_that_compares_size_with_strlen() {
let c = emitted("@final struct S { string text; };", "S");
assert!(c.contains("ZDROS_STRING_OUT(&sample.text);"), "{c}");
}
#[test]
fn a_wstring_takes_its_count_from_the_holder_and_not_from_a_terminator() {
let c = emitted("@final struct S { wstring w; };", "S");
assert!(
c.contains("zdj_wstring((const unsigned short *)sample.w.data, sample.w.size);"),
"{c}"
);
}
#[test]
fn a_signed_char_holder_is_widened_through_unsigned_char() {
let c = emitted("@final struct S { char c; };", "S");
assert!(
c.contains("zdj_char((char)(unsigned char)(sample.c));"),
"{c}"
);
}
#[test]
fn a_sequence_takes_its_length_from_the_bytes_and_not_from_the_type() {
let c = emitted("@final struct S { sequence<long> q; };", "S");
assert!(c.contains("size_t zd_n = sample.q.size;"), "{c}");
assert!(c.contains("sample.q.data[zd_i]"), "{c}");
}
#[test]
fn an_array_is_unrolled_because_its_length_is_the_types() {
let c = emitted("@final struct S { long a[3]; };", "S");
assert!(c.contains("sample.a[0]"), "{c}");
assert!(c.contains("sample.a[2]"), "{c}");
assert!(!c.contains("sample.a[3]"), "{c}");
}
#[test]
fn a_sixty_four_bit_integer_prints_as_a_string_like_every_other_column() {
let c = emitted(
"@final struct S { long long a; unsigned long long b; };",
"S",
);
assert!(c.contains("zdj_i64_string"), "{c}");
assert!(c.contains("zdj_u64_string"), "{c}");
}
}