use crate::contract;
use crate::encode::ProbeError;
use crate::sink::{Aggregate, Sink};
use crate::source::Source;
use std::string::String;
use std::vec::Vec;
use zerodds_ast::{Ast, CharWidth, PrimitiveKind, TypeKind, TypeNode};
use zerodds_ast_core::Id;
pub fn canonicalise<V: Source + Sink>(
ast: &Ast,
type_: Id<TypeNode>,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
contract::reject_alias_cycle(ast)?;
canonicalise_at(ast, type_, value, path)
}
fn canonicalise_at<V: Source + Sink>(
ast: &Ast,
type_: Id<TypeNode>,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
match &node.kind {
TypeKind::Primitive(kind) => primitive(*kind, value, path),
TypeKind::String { width, .. } => {
let text = contract::text(value, path)?;
match width {
CharWidth::Char | CharWidth::WChar => Ok(V::text(text.to_string())),
}
}
TypeKind::Fixed { digits, scale } => {
let text = contract::text(value, path)?;
Ok(V::fixed(decimal(text, *digits, *scale, path)?))
}
TypeKind::Alias { aliased } => {
let target = aliased.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the typedef target is unresolved"),
})?;
canonicalise_at(ast, target, value, path)
}
TypeKind::Enumeration { .. } => {
let (name, _) = contract::enumerator(ast, type_, value, path)?;
Ok(V::enumerator(name))
}
TypeKind::Bitmask { .. } => bitmask(ast, type_, value, path),
TypeKind::Bitset { .. } => {
let object = contract::object(value, path)?;
let declared = contract::bitfield_names(ast, &ast.all_bitfields_of(type_));
contract::reject_unknown_keys(object, &declared, path)?;
let mut out = Vec::with_capacity(declared.len());
for name in &declared {
let field_path = contract::join(path, name);
let Some(field) = object.get(name) else {
return Err(ProbeError::Missing { path: field_path });
};
out.push((name.clone(), contract::integer(field, &field_path)?));
}
V::bitfields(out, path)
}
TypeKind::Sequence { element, .. } | TypeKind::Array { element, .. } => {
let element = element.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the element type is unresolved"),
})?;
let dims = match &node.kind {
TypeKind::Array { dims, .. } => dims.len(),
_ => 1,
};
collection(ast, element, value, path, dims)
}
TypeKind::Map {
key, value: entry, ..
} => {
let key_type = key.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the map key type is unresolved"),
})?;
let value_type = entry.resolved().ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the map value type is unresolved"),
})?;
let entries = contract::array(value, path)?;
let mut out = Vec::with_capacity(entries.len());
for (index, pair) in entries.iter().enumerate() {
let entry_path = format!("{path}[{index}]");
let (entry_key, entry_value) = contract::map_entry(pair, &entry_path)?;
out.push(V::map_entry(
canonicalise_at(ast, key_type, entry_key, &format!("{entry_path}.key"))?,
canonicalise_at(ast, value_type, entry_value, &format!("{entry_path}.value"))?,
));
}
out.sort_by_key(|entry| {
entry
.as_items()
.and_then(<[V]>::first)
.map_or_else(String::new, Source::render)
});
Ok(V::collection(out))
}
TypeKind::Structure { .. } | TypeKind::Valuetype { .. } => {
aggregate(ast, type_, value, path)
}
TypeKind::Union { .. } => union(ast, type_, value, path),
other => Err(ProbeError::Unfilled {
path: path.to_string(),
kind: other.as_str(),
}),
}
}
fn aggregate<V: Source + Sink>(
ast: &Ast,
type_: Id<TypeNode>,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
let object = contract::object(value, path)?;
let members = contract::members(ast, type_);
let declared: Vec<String> = members.iter().map(|member| member.name.clone()).collect();
contract::reject_unknown_keys(object, &declared, path)?;
let mut out = Aggregate::new();
for member in members {
let Some(present) = contract::member_value(object, &member, path)? else {
continue;
};
let member_path = contract::join(path, &member.name);
out.insert(
member.name.clone(),
canonicalise_at(ast, member.type_, present, &member_path)?,
);
}
Ok(V::aggregate(out))
}
fn union<V: Source + Sink>(
ast: &Ast,
type_: Id<TypeNode>,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
let object = contract::object(value, path)?;
let node = ast.type_(type_).map_err(|error| ProbeError::NoLayout {
detail: format!("{error}"),
})?;
let TypeKind::Union { discriminator, .. } = &node.kind else {
return Err(ProbeError::Unsupported {
path: path.to_string(),
kind: "a union whose kind changed under the probe",
});
};
let discriminator = discriminator
.resolved()
.ok_or_else(|| ProbeError::NoLayout {
detail: format!("{path}: the discriminator is unresolved"),
})?;
let cases = contract::union_members(ast, type_);
let mut out = Aggregate::new();
let discriminant = contract::discriminator(object, path)?;
out.insert(
"_d".to_string(),
canonicalise_at(
ast,
discriminator,
discriminant,
&contract::join(path, "_d"),
)?,
);
for (name, member_value) in object {
if name == "_d" {
continue;
}
let member_type = cases
.iter()
.find(|(case_name, _)| case_name == name)
.map(|(_, member_type)| *member_type)
.ok_or_else(|| ProbeError::Unknown {
path: format!("{path}.{name}"),
declared: cases.iter().map(|(name, _)| name.clone()).collect(),
})?;
out.insert(
name.to_string(),
canonicalise_at(ast, member_type, member_value, &format!("{path}.{name}"))?,
);
}
Ok(V::aggregate(out))
}
fn collection<V: Source + Sink>(
ast: &Ast,
element: Id<TypeNode>,
value: &V,
path: &str,
dims: usize,
) -> Result<V, ProbeError> {
let items = contract::array(value, path)?;
let mut out = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
let item_path = format!("{path}[{index}]");
out.push(if dims > 1 {
collection(ast, element, item, &item_path, dims - 1)?
} else {
canonicalise_at(ast, element, item, &item_path)?
});
}
Ok(V::collection(out))
}
fn bitmask<V: Source + Sink>(
ast: &Ast,
type_: Id<TypeNode>,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
let mut set = Vec::new();
for (name, _) in contract::bit_names(ast, type_, value, path)? {
set.push(name);
}
let mut out = Vec::new();
for id in ast.bits_of(type_) {
let Ok(node) = ast.bit_value(*id) else {
continue;
};
if set.contains(&node.name) {
out.push(node.name.clone());
}
}
Ok(V::bit_names(out))
}
fn primitive<V: Source + Sink>(
kind: PrimitiveKind,
value: &V,
path: &str,
) -> Result<V, ProbeError> {
match kind {
PrimitiveKind::Boolean => Ok(V::boolean(contract::boolean(value, path)?)),
PrimitiveKind::Char | PrimitiveKind::WChar => Ok(V::character(
kind,
contract::character_for(kind, value, path)?,
)),
PrimitiveKind::Float32 => {
#[allow(clippy::cast_possible_truncation)]
let narrowed = f64::from(contract::float(value, path)? as f32);
V::float(kind, narrowed, path)
}
PrimitiveKind::Float64 => V::float(kind, contract::float(value, path)?, path),
PrimitiveKind::Float128 => Ok(V::float128(contract::float128(value, path)?)),
PrimitiveKind::Octet
| PrimitiveKind::UInt8
| PrimitiveKind::Int8
| PrimitiveKind::Int16
| PrimitiveKind::UInt16
| PrimitiveKind::Int32
| PrimitiveKind::UInt32
| PrimitiveKind::Int64
| PrimitiveKind::UInt64 => {
V::integer(kind, contract::integer_for(kind, value, path)?, path)
}
}
}
pub(crate) fn decimal(text: &str, digits: u8, scale: u8, path: &str) -> Result<String, ProbeError> {
let parts =
zerodds_wire::parse_fixed(text, digits, scale, 0).map_err(|_| ProbeError::Mismatch {
path: path.to_string(),
expected: format!("a decimal that fits fixed<{digits}, {scale}>"),
found: format!("`{text}`"),
})?;
let mut out = String::new();
let all_zero = parts.whole.is_empty() && parts.fraction.bytes().all(|byte| byte == b'0');
if parts.negative && !all_zero {
out.push('-');
}
out.push_str(if parts.whole.is_empty() {
"0"
} else {
parts.whole
});
if scale > 0 {
out.push('.');
out.push_str(parts.fraction);
for _ in parts.fraction.len()..usize::from(scale) {
out.push('0');
}
}
Ok(out)
}
#[must_use]
pub fn text<V: Source>(value: &V) -> String {
value.render()
}
#[must_use]
pub fn difference<V: Source>(expected: &V, found: &V, path: &str) -> Option<Difference> {
if expected.is_object() && found.is_object() {
let mut keys: Vec<&str> = expected
.entries()
.into_iter()
.map(|(name, _)| name)
.chain(found.entries().into_iter().map(|(name, _)| name))
.collect();
keys.sort_unstable();
keys.dedup();
for key in keys {
let member = format!("{path}.{key}");
match (expected.field(key), found.field(key)) {
(Some(a), Some(b)) => {
if let Some(inner) = difference(a, b, &member) {
return Some(inner);
}
}
(a, b) => {
return Some(Difference {
path: member,
expected: a.map(text),
found: b.map(text),
});
}
}
}
return None;
}
if let (Some(left), Some(right)) = (expected.as_items(), found.as_items()) {
for index in 0..left.len().max(right.len()) {
let element = format!("{path}[{index}]");
match (left.get(index), right.get(index)) {
(Some(a), Some(b)) => {
if let Some(inner) = difference(a, b, &element) {
return Some(inner);
}
}
(a, b) => {
return Some(Difference {
path: element,
expected: a.map(text),
found: b.map(text),
});
}
}
}
return None;
}
if text(expected) == text(found) {
return None;
}
Some(Difference {
path: path.to_string(),
expected: Some(text(expected)),
found: Some(text(found)),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Difference {
pub path: String,
pub expected: Option<String>,
pub found: Option<String>,
}
impl core::fmt::Display for Difference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let expected = self.expected.as_deref().unwrap_or("<absent>");
let found = self.found.as_deref().unwrap_or("<absent>");
write!(f, "{}: wrote {expected}, read {found}", self.path)
}
}