use std::collections::{BTreeMap, BTreeSet};
use ridl_ir::v2;
use crate::classify::{struct_slots, union_slots};
use crate::{Category, Change, emit, frozen};
pub(crate) fn walk_packages(old: &v2::Package, new: &v2::Package, changes: &mut Vec<Change>) {
let pkg = new.name.as_str();
diff_decls(pkg, &old.decls, &new.decls, changes);
diff_interfaces(pkg, old, new, changes);
diff_services(pkg, &old.services, &new.services, changes);
}
fn diff_decls(pkg: &str, old: &[v2::Decl], new: &[v2::Decl], changes: &mut Vec<Change>) {
let old_by: BTreeMap<&str, &v2::Decl> = old.iter().map(|d| (d.name.as_str(), d)).collect();
let new_by: BTreeMap<&str, &v2::Decl> = new.iter().map(|d| (d.name.as_str(), d)).collect();
for (name, old_decl) in &old_by {
match new_by.get(name) {
Some(new_decl) => diff_decl(pkg, name, old_decl, new_decl, changes),
None => emit(
changes,
format!("{pkg}/{name}"),
Category::DeclRemoved,
Some(decl_kind_name(old_decl).to_string()),
None,
),
}
}
for (name, new_decl) in &new_by {
if !old_by.contains_key(name) {
emit(
changes,
format!("{pkg}/{name}"),
Category::DeclAdded,
None,
Some(decl_kind_name(new_decl).to_string()),
);
}
}
}
fn diff_decl(pkg: &str, name: &str, old: &v2::Decl, new: &v2::Decl, changes: &mut Vec<Change>) {
let path = format!("{pkg}/{name}");
if envelope_differs(old, new) || member_docs_differ(&old.kind, &new.kind) {
emit(changes, path.clone(), Category::DocOnly, None, None);
}
emit_visibility(changes, path.clone(), old.visibility, new.visibility);
use v2::decl::Kind;
match (
&without_member_docs(&old.kind),
&without_member_docs(&new.kind),
) {
(Some(Kind::TypeDef(a)), Some(Kind::TypeDef(b))) => diff_type_def(&path, a, b, changes),
(Some(Kind::ConstDef(a)), Some(Kind::ConstDef(b))) => {
if a != b {
emit(
changes,
path,
Category::InitChanged,
Some(const_str(a)),
Some(const_str(b)),
);
}
}
(Some(Kind::StructDef(a)), Some(Kind::StructDef(b))) => {
diff_composite(
&path,
struct_slots(a),
struct_slots(b),
"ordinal",
a == b,
|| struct_ignoring_order(a) == struct_ignoring_order(b),
changes,
);
}
(Some(Kind::EnumDef(a)), Some(Kind::EnumDef(b))) => {
diff_composite(
&path,
positions(&a.values),
positions(&b.values),
"position",
a == b,
|| enum_ignoring_order(a) == enum_ignoring_order(b),
changes,
);
}
(Some(Kind::EnumSetDef(a)), Some(Kind::EnumSetDef(b))) => {
diff_composite(
&path,
positions(&a.bits),
positions(&b.bits),
"position",
a == b,
|| enum_set_ignoring_order(a) == enum_set_ignoring_order(b),
changes,
);
}
(Some(Kind::UnionDef(a)), Some(Kind::UnionDef(b))) => {
diff_composite(
&path,
union_slots(a),
union_slots(b),
"ordinal",
a == b,
|| union_ignoring_order(a) == union_ignoring_order(b),
changes,
);
}
(old_kind, new_kind) => {
if kind_discriminant(old_kind) != kind_discriminant(new_kind) {
emit(
changes,
path,
Category::KindChanged,
Some(kind_name_opt(old_kind).to_string()),
Some(kind_name_opt(new_kind).to_string()),
);
}
}
}
}
fn diff_type_def(path: &str, a: &v2::TypeDef, b: &v2::TypeDef, changes: &mut Vec<Change>) {
if a.backing != b.backing || a.width != b.width {
emit(
changes,
path.to_string(),
Category::WidthChanged,
Some(type_repr_str(a)),
Some(type_repr_str(b)),
);
}
if a.constraint != b.constraint {
emit(
changes,
path.to_string(),
Category::ConstraintChanged,
Some(constraint_str(a.constraint.as_ref())),
Some(constraint_str(b.constraint.as_ref())),
);
}
if a.declared_init != b.declared_init || a.init != b.init {
emit(
changes,
path.to_string(),
Category::InitChanged,
Some(type_init_str(a)),
Some(type_init_str(b)),
);
}
}
fn diff_composite(
path: &str,
old: Vec<(String, i64)>,
new: Vec<(String, i64)>,
slot: &str,
equal: bool,
equal_ignoring_order: impl FnOnce() -> bool,
changes: &mut Vec<Change>,
) {
if equal {
return;
}
let old_set: BTreeSet<&String> = old.iter().map(|(name, _)| name).collect();
let new_set: BTreeSet<&String> = new.iter().map(|(name, _)| name).collect();
let mut structural = false;
for (name, _) in &new {
if !old_set.contains(name) {
emit(
changes,
format!("{path}/{name}"),
Category::DeclAdded,
None,
Some(name.clone()),
);
structural = true;
}
}
for (name, _) in &old {
if !new_set.contains(name) {
emit(
changes,
format!("{path}/{name}"),
Category::DeclRemoved,
Some(name.clone()),
None,
);
structural = true;
}
}
if structural {
return;
}
let mut moved = false;
for (name, new_slot) in &new {
let (_, old_slot) = old
.iter()
.find(|(old_name, _)| old_name == name)
.expect("the name sets are equal, so every new name appears in the old body");
if old_slot != new_slot {
moved = true;
emit(
changes,
format!("{path}/{name}"),
Category::MemberReordered,
Some(format!("{slot} {old_slot}")),
Some(format!("{slot} {new_slot}")),
);
}
}
if !moved || !equal_ignoring_order() {
emit(
changes,
path.to_string(),
Category::ConstraintChanged,
None,
None,
);
}
}
type ShapeKey<'a> = (bool, &'a str);
fn shapes_by_key(package: &v2::Package) -> BTreeMap<ShapeKey<'_>, v2::InterfaceShape<'_>> {
package
.shapes()
.map(|shape| ((shape.is_inline(), shape.name), shape))
.collect()
}
fn diff_interfaces(pkg: &str, old: &v2::Package, new: &v2::Package, changes: &mut Vec<Change>) {
let old_shapes = shapes_by_key(old);
let new_shapes = shapes_by_key(new);
let new_by_number: BTreeMap<u32, ShapeKey<'_>> = new_shapes
.iter()
.rev()
.filter(|(_, shape)| frozen(shape.interface))
.map(|(key, shape)| (shape.interface.number, *key))
.collect();
let mut pairs: BTreeMap<ShapeKey<'_>, ShapeKey<'_>> = BTreeMap::new();
let mut taken: BTreeSet<ShapeKey<'_>> = BTreeSet::new();
for (old_key, old_shape) in &old_shapes {
if !frozen(old_shape.interface) {
continue;
}
if let Some(new_key) = new_by_number.get(&old_shape.interface.number)
&& taken.insert(*new_key)
{
pairs.insert(*old_key, *new_key);
}
}
for (old_key, old_shape) in &old_shapes {
if frozen(old_shape.interface) {
continue;
}
if new_shapes.contains_key(old_key) && taken.insert(*old_key) {
pairs.insert(*old_key, *old_key);
}
}
for (old_key, old_shape) in &old_shapes {
let Some(new_key) = pairs.get(old_key) else {
let number = old_shape.interface.number;
let sanctioned = frozen(old_shape.interface)
&& new.retired.iter().any(|entry| entry.number == number);
if sanctioned {
emit(
changes,
format!("{pkg}/{}", old_shape.name),
Category::InterfaceRetired,
Some("interface".to_string()),
Some("retired".to_string()),
);
} else {
emit(
changes,
format!("{pkg}/{}", old_shape.name),
Category::DeclRemoved,
Some("interface".to_string()),
None,
);
}
continue;
};
let new_shape = &new_shapes[new_key];
let name = new_shape.name;
if old_shape.name != name {
emit(
changes,
format!("{pkg}/{name}"),
Category::InterfaceRenamed,
Some(old_shape.name.to_string()),
Some(name.to_string()),
);
}
if !new_shape.is_inline() {
if interface_envelope_differs(old_shape.interface, new_shape.interface) {
emit(
changes,
format!("{pkg}/{name}"),
Category::DocOnly,
None,
None,
);
}
emit_visibility(
changes,
format!("{pkg}/{name}"),
old_shape.visibility(),
new_shape.visibility(),
);
}
diff_interface(pkg, name, old_shape.interface, new_shape.interface, changes);
}
for (new_key, new_shape) in &new_shapes {
if !taken.contains(new_key) {
emit(
changes,
format!("{pkg}/{}", new_shape.name),
Category::DeclAdded,
None,
Some("interface".to_string()),
);
}
}
}
fn diff_interface(
pkg: &str,
iface: &str,
old: &v2::Interface,
new: &v2::Interface,
changes: &mut Vec<Change>,
) {
let old_live = live_interactions(old);
let new_live = live_interactions(new);
let old_reserved = reserved_names(old);
let new_reserved = reserved_names(new);
let old_live_names: BTreeSet<&str> = old_live.iter().map(|(name, ..)| *name).collect();
let new_live_map: BTreeMap<&str, (u32, &v2::Decl)> = new_live
.iter()
.map(|(name, ord, decl)| (*name, (*ord, *decl)))
.collect();
let mut matched: Vec<(&str, u32, u32)> = Vec::new();
for (name, old_ord, old_decl) in &old_live {
let member_path = format!("{pkg}/{iface}/{name}");
if let Some((new_ord, new_decl)) = new_live_map.get(name) {
matched.push((name, *old_ord, *new_ord));
diff_interaction(iface, &member_path, old_decl, new_decl, changes);
} else if let Some(reserved_ord) = new_reserved.get(name) {
if *reserved_ord == *old_ord {
emit(
changes,
member_path,
Category::InteractionRetired,
Some(interaction_desc(old_decl)),
Some("reserved".to_string()),
);
} else {
emit(
changes,
member_path,
Category::InteractionRemoved,
Some(format!(
"{} at ordinal {old_ord}",
interaction_desc(old_decl)
)),
Some(format!("reserved at ordinal {reserved_ord}")),
);
}
} else {
emit(
changes,
member_path,
Category::InteractionRemoved,
Some(interaction_desc(old_decl)),
None,
);
}
}
for (name, old_ord) in &old_reserved {
if new_live_map.contains_key(name) {
continue; }
match new_reserved.get(name) {
Some(new_ord) if new_ord == old_ord => {}
Some(new_ord) => emit(
changes,
format!("{pkg}/{iface}/{name}"),
Category::InteractionReordered,
Some(format!("reserved at ordinal {old_ord}")),
Some(format!("reserved at ordinal {new_ord}")),
),
None => emit(
changes,
format!("{pkg}/{iface}/{name}"),
Category::InteractionRemoved,
Some(format!("reserved at ordinal {old_ord}")),
None,
),
}
}
let anchor_ordinals = anchor_ordinals(&matched, new, &old_live_names, &old_reserved);
for (name, new_ord) in &new_reserved {
if old_live_names.contains(name) || old_reserved.contains_key(name) {
continue;
}
let category = if anchor_ordinals.iter().any(|&anchor| anchor > *new_ord) {
Category::InteractionInserted
} else {
Category::InteractionAppended
};
emit(
changes,
format!("{pkg}/{iface}/{name}"),
category,
None,
Some(format!("reserved at ordinal {new_ord}")),
);
}
for (name, new_ord, new_decl) in &new_live {
if old_live_names.contains(name) {
continue;
}
let member_path = format!("{pkg}/{iface}/{name}");
if old_reserved.contains_key(name) {
emit(
changes,
member_path,
Category::ReservedNameRedeclared,
Some("reserved".to_string()),
Some(interaction_desc(new_decl)),
);
} else {
let category = if anchor_ordinals.iter().any(|&anchor| anchor > *new_ord) {
Category::InteractionInserted
} else {
Category::InteractionAppended
};
emit(
changes,
member_path,
category,
None,
Some(interaction_desc(new_decl)),
);
}
}
detect_reorders(pkg, iface, &matched, changes);
}
fn detect_reorders(
pkg: &str,
iface: &str,
matched: &[(&str, u32, u32)],
changes: &mut Vec<Change>,
) {
let old_rank = rank_by(matched, |(_, old_ord, _)| *old_ord);
let new_rank = rank_by(matched, |(_, _, new_ord)| *new_ord);
for (name, old_ord, new_ord) in matched {
if old_rank[name] != new_rank[name] {
emit(
changes,
format!("{pkg}/{iface}/{name}"),
Category::InteractionReordered,
Some(old_ord.to_string()),
Some(new_ord.to_string()),
);
}
}
}
fn rank_by<'a>(
matched: &[(&'a str, u32, u32)],
key: impl Fn(&(&'a str, u32, u32)) -> u32,
) -> BTreeMap<&'a str, usize> {
let mut order: Vec<&(&'a str, u32, u32)> = matched.iter().collect();
order.sort_by_key(|entry| key(entry));
order
.iter()
.enumerate()
.map(|(rank, (name, _, _))| (*name, rank))
.collect()
}
fn anchor_ordinals(
matched: &[(&str, u32, u32)],
new: &v2::Interface,
old_live_names: &BTreeSet<&str>,
old_reserved: &BTreeMap<&str, u32>,
) -> Vec<u32> {
let mut ordinals: Vec<u32> = matched.iter().map(|(_, _, new_ord)| *new_ord).collect();
for decl in &new.interactions {
if let Some(v2::decl::Kind::ReservedSlot(reserved)) = &decl.kind
&& let Some(name) = reserved.name.as_deref()
&& (old_live_names.contains(name) || old_reserved.contains_key(name))
{
ordinals.push(decl.ordinal);
}
}
ordinals
}
fn diff_interaction(
iface: &str,
path: &str,
old: &v2::Decl,
new: &v2::Decl,
changes: &mut Vec<Change>,
) {
if envelope_differs(old, new) || member_docs_differ(&old.kind, &new.kind) {
emit(changes, path.to_string(), Category::DocOnly, None, None);
}
emit_visibility(changes, path.to_string(), old.visibility, new.visibility);
use v2::decl::Kind;
match (
&without_member_docs(&old.kind),
&without_member_docs(&new.kind),
) {
(Some(Kind::SignalDef(a)), Some(Kind::SignalDef(b))) => {
if a.payload != b.payload {
emit(
changes,
path.to_string(),
Category::PayloadChanged,
Some(a.payload.clone()),
Some(b.payload.clone()),
);
}
if a.declared_init != b.declared_init || a.init != b.init {
emit(
changes,
path.to_string(),
Category::InitChanged,
Some(signal_init_str(a)),
Some(signal_init_str(b)),
);
}
if a.timing != b.timing {
emit(
changes,
path.to_string(),
Category::TimingChanged,
Some(timing_str(a.timing.as_ref())),
Some(timing_str(b.timing.as_ref())),
);
}
}
(Some(Kind::EventDef(a)), Some(Kind::EventDef(b))) => {
if a.payload != b.payload {
emit(
changes,
path.to_string(),
Category::PayloadChanged,
Some(a.payload.clone()),
Some(b.payload.clone()),
);
}
if a.timing != b.timing {
emit(
changes,
path.to_string(),
Category::TimingChanged,
Some(timing_str(a.timing.as_ref())),
Some(timing_str(b.timing.as_ref())),
);
}
}
(Some(Kind::CommandDef(a)), Some(Kind::CommandDef(b))) => {
if a.params != b.params {
emit(
changes,
path.to_string(),
Category::ParamsChanged,
Some(params_str(&a.params)),
Some(params_str(&b.params)),
);
}
if a.contracts != b.contracts {
emit(
changes,
path.to_string(),
Category::ContractChanged,
Some(contracts_str(&a.contracts)),
Some(contracts_str(&b.contracts)),
);
}
if a.timing != b.timing {
emit(
changes,
path.to_string(),
Category::RpcBoundChanged,
Some(timing_str(a.timing.as_ref())),
Some(timing_str(b.timing.as_ref())),
);
}
}
(Some(Kind::QueryDef(a)), Some(Kind::QueryDef(b))) => {
if a.params != b.params {
emit(
changes,
path.to_string(),
Category::ParamsChanged,
Some(params_str(&a.params)),
Some(params_str(&b.params)),
);
}
if a.return_type != b.return_type {
emit(
changes,
path.to_string(),
Category::ReturnChanged,
Some(return_str(iface, old.ordinal, a.return_type.as_ref())),
Some(return_str(iface, new.ordinal, b.return_type.as_ref())),
);
}
if a.contracts != b.contracts {
emit(
changes,
path.to_string(),
Category::ContractChanged,
Some(contracts_str(&a.contracts)),
Some(contracts_str(&b.contracts)),
);
}
if a.timing != b.timing {
emit(
changes,
path.to_string(),
Category::RpcBoundChanged,
Some(timing_str(a.timing.as_ref())),
Some(timing_str(b.timing.as_ref())),
);
}
}
(Some(Kind::FixedDef(a)), Some(Kind::FixedDef(b))) => {
if a.payload != b.payload {
emit(
changes,
path.to_string(),
Category::PayloadChanged,
Some(field_type_opt_str(a.payload.as_ref())),
Some(field_type_opt_str(b.payload.as_ref())),
);
}
}
(old_kind, new_kind) => {
emit(
changes,
path.to_string(),
Category::KindChanged,
Some(kind_name_opt(old_kind).to_string()),
Some(kind_name_opt(new_kind).to_string()),
);
}
}
}
fn diff_services(pkg: &str, old: &[v2::Service], new: &[v2::Service], changes: &mut Vec<Change>) {
let old_by: BTreeMap<&str, &v2::Service> = old.iter().map(|s| (s.name.as_str(), s)).collect();
let new_by: BTreeMap<&str, &v2::Service> = new.iter().map(|s| (s.name.as_str(), s)).collect();
for (name, old_svc) in &old_by {
match new_by.get(name) {
Some(new_svc) => diff_service(pkg, name, old_svc, new_svc, changes),
None => emit(
changes,
format!("{pkg}/{name}"),
Category::DeclRemoved,
Some("service".to_string()),
None,
),
}
}
for name in new_by.keys() {
if !old_by.contains_key(name) {
emit(
changes,
format!("{pkg}/{name}"),
Category::DeclAdded,
None,
Some("service".to_string()),
);
}
}
}
fn diff_service(
pkg: &str,
name: &str,
old: &v2::Service,
new: &v2::Service,
changes: &mut Vec<Change>,
) {
let path = format!("{pkg}/{name}");
if service_envelope_differs(old, new) {
emit(changes, path.clone(), Category::DocOnly, None, None);
}
emit_visibility(changes, path.clone(), old.visibility, new.visibility);
match (inline_shape(old), inline_shape(new)) {
(Some(_), Some(_)) => {}
(None, None) => diff_service_set(pkg, name, old, new, changes),
(old_inline, new_inline) => {
emit(
changes,
path,
Category::ServiceChanged,
Some(form_desc(old_inline.is_some()).to_string()),
Some(form_desc(new_inline.is_some()).to_string()),
);
}
}
}
fn diff_service_set(
pkg: &str,
svc: &str,
old: &v2::Service,
new: &v2::Service,
changes: &mut Vec<Change>,
) {
let old_refs = interface_refs(old);
let new_refs = interface_refs(new);
for reference in old_refs.difference(&new_refs) {
emit(
changes,
format!("{pkg}/{svc}/{reference}"),
Category::ServiceInterfaceRemoved,
Some((*reference).to_string()),
None,
);
}
for reference in new_refs.difference(&old_refs) {
emit(
changes,
format!("{pkg}/{svc}/{reference}"),
Category::ServiceInterfaceAdded,
None,
Some((*reference).to_string()),
);
}
}
fn live_interactions(iface: &v2::Interface) -> Vec<(&str, u32, &v2::Decl)> {
iface
.interactions
.iter()
.filter_map(|decl| match &decl.kind {
Some(v2::decl::Kind::ReservedSlot(_)) | None => None,
Some(_) => Some((decl.name.as_str(), decl.ordinal, decl)),
})
.collect()
}
fn reserved_names(iface: &v2::Interface) -> BTreeMap<&str, u32> {
iface
.interactions
.iter()
.filter_map(|decl| match &decl.kind {
Some(v2::decl::Kind::ReservedSlot(reserved)) => {
reserved.name.as_deref().map(|name| (name, decl.ordinal))
}
_ => None,
})
.collect()
}
fn inline_shape(service: &v2::Service) -> Option<&v2::Interface> {
service.shapes.iter().find_map(|slot| match &slot.kind {
Some(v2::service_shape::Kind::Inline(interface)) => Some(interface),
_ => None,
})
}
fn interface_refs(service: &v2::Service) -> BTreeSet<&str> {
service
.shapes
.iter()
.filter_map(|slot| match &slot.kind {
Some(v2::service_shape::Kind::InterfaceRef(reference)) => Some(reference.as_str()),
_ => None,
})
.collect()
}
fn positions(values: &[v2::EnumValue]) -> Vec<(String, i64)> {
values
.iter()
.zip(1..)
.map(|(value, position)| (value.name.clone(), position))
.collect()
}
fn struct_ignoring_order(def: &v2::StructDef) -> v2::StructDef {
use v2::struct_member::Member;
let mut def = def.clone();
for member in &mut def.members {
match &mut member.member {
Some(Member::Field(field)) => field.ordinal = 0,
Some(Member::Reserved(reserved)) => reserved.ordinal = 0,
None => {}
}
}
def.members.sort_by_key(|member| match &member.member {
Some(Member::Field(field)) => (0, Some(field.name.clone()), None),
Some(Member::Reserved(reserved)) => (1, reserved.name.clone(), reserved.value),
None => (2, None, None),
});
def
}
fn enum_ignoring_order(def: &v2::EnumDef) -> v2::EnumDef {
let mut def = def.clone();
def.values.sort_by_key(|value| value.name.clone());
def.reserved.sort_by_key(tombstone_key);
def
}
fn enum_set_ignoring_order(def: &v2::EnumSetDef) -> v2::EnumSetDef {
let mut def = def.clone();
def.bits.sort_by_key(|bit| bit.name.clone());
def
}
fn union_ignoring_order(def: &v2::UnionDef) -> v2::UnionDef {
let mut def = def.clone();
for arm in &mut def.arms {
arm.ordinal = 0;
}
def.arms.sort_by_key(|arm| arm.name.clone());
for reserved in &mut def.reserved {
reserved.ordinal = 0;
}
def.reserved.sort_by_key(tombstone_key);
def
}
fn tombstone_key(reserved: &v2::Reserved) -> (Option<String>, Option<i64>) {
(reserved.name.clone(), reserved.value)
}
fn envelope_differs(old: &v2::Decl, new: &v2::Decl) -> bool {
old.doc != new.doc
|| old.labels != new.labels
|| old.deprecated != new.deprecated
|| old.links != new.links
|| old.see != new.see
|| old.since != new.since
}
fn interface_envelope_differs(old: &v2::Interface, new: &v2::Interface) -> bool {
old.doc != new.doc
|| old.labels != new.labels
|| old.deprecated != new.deprecated
|| old.links != new.links
|| old.see != new.see
|| old.since != new.since
}
fn service_envelope_differs(old: &v2::Service, new: &v2::Service) -> bool {
old.doc != new.doc
|| old.labels != new.labels
|| old.deprecated != new.deprecated
|| old.links != new.links
|| old.see != new.see
|| old.since != new.since
}
#[derive(PartialEq)]
struct MemberDocs<'a> {
doc: &'a str,
labels: &'a [String],
deprecated: Option<&'a str>,
links: &'a [v2::DocLink],
see: &'a [v2::DocLink],
since: &'a [String],
}
fn member_docs(kind: &Option<v2::decl::Kind>) -> BTreeMap<&str, MemberDocs<'_>> {
use v2::decl::Kind;
let mut docs = BTreeMap::new();
match kind {
Some(Kind::StructDef(def)) => {
for member in &def.members {
if let Some(v2::struct_member::Member::Field(field)) = &member.member {
docs.insert(
field.name.as_str(),
MemberDocs {
doc: &field.doc,
labels: &field.labels,
deprecated: field.deprecated.as_deref(),
links: &field.links,
see: &field.see,
since: &field.since,
},
);
}
}
}
Some(Kind::EnumDef(v2::EnumDef { values, .. }))
| Some(Kind::EnumSetDef(v2::EnumSetDef { bits: values, .. })) => {
for value in values {
docs.insert(
value.name.as_str(),
MemberDocs {
doc: &value.doc,
labels: &[],
deprecated: None,
links: &value.links,
see: &value.see,
since: &value.since,
},
);
}
}
Some(Kind::UnionDef(def)) => {
for arm in &def.arms {
docs.insert(
arm.name.as_str(),
MemberDocs {
doc: &arm.doc,
labels: &[],
deprecated: None,
links: &arm.links,
see: &arm.see,
since: &arm.since,
},
);
}
}
Some(Kind::CommandDef(v2::CommandDef { params, .. }))
| Some(Kind::QueryDef(v2::QueryDef { params, .. })) => {
for param in params {
docs.insert(
param.name.as_str(),
MemberDocs {
doc: ¶m.doc,
labels: &[],
deprecated: None,
links: ¶m.links,
see: ¶m.see,
since: ¶m.since,
},
);
}
}
Some(Kind::TypeDef(_))
| Some(Kind::ConstDef(_))
| Some(Kind::SignalDef(_))
| Some(Kind::EventDef(_))
| Some(Kind::FixedDef(_))
| Some(Kind::ReservedSlot(_))
| None => {}
}
docs
}
fn member_docs_differ(old: &Option<v2::decl::Kind>, new: &Option<v2::decl::Kind>) -> bool {
let new_docs = member_docs(new);
member_docs(old).iter().any(|(name, old_docs)| {
new_docs
.get(name)
.is_some_and(|new_docs| new_docs != old_docs)
})
}
fn without_member_docs(kind: &Option<v2::decl::Kind>) -> Option<v2::decl::Kind> {
use v2::decl::Kind;
let mut kind = kind.clone();
match &mut kind {
Some(Kind::StructDef(def)) => {
for member in &mut def.members {
if let Some(v2::struct_member::Member::Field(field)) = &mut member.member {
field.doc.clear();
field.labels.clear();
field.deprecated = None;
field.links.clear();
field.see.clear();
field.since.clear();
}
}
}
Some(Kind::EnumDef(v2::EnumDef { values, .. }))
| Some(Kind::EnumSetDef(v2::EnumSetDef { bits: values, .. })) => {
for value in values {
value.doc.clear();
value.links.clear();
value.see.clear();
value.since.clear();
}
}
Some(Kind::UnionDef(def)) => {
for arm in &mut def.arms {
arm.doc.clear();
arm.links.clear();
arm.see.clear();
arm.since.clear();
}
}
Some(Kind::CommandDef(v2::CommandDef { params, .. }))
| Some(Kind::QueryDef(v2::QueryDef { params, .. })) => {
for param in params {
param.doc.clear();
param.links.clear();
param.see.clear();
param.since.clear();
}
}
Some(Kind::TypeDef(_))
| Some(Kind::ConstDef(_))
| Some(Kind::SignalDef(_))
| Some(Kind::EventDef(_))
| Some(Kind::FixedDef(_))
| Some(Kind::ReservedSlot(_))
| None => {}
}
kind
}
fn emit_visibility(changes: &mut Vec<Change>, path: String, old: i32, new: i32) {
if old == new {
return;
}
emit(
changes,
path,
Category::VisibilityChanged,
Some(visibility_name(old).to_string()),
Some(visibility_name(new).to_string()),
);
}
fn visibility_name(visibility: i32) -> &'static str {
match v2::Visibility::try_from(visibility) {
Ok(v2::Visibility::Public) => "public",
Ok(v2::Visibility::Internal) => "internal",
_ => "unspecified",
}
}
fn decl_kind_name(decl: &v2::Decl) -> &'static str {
kind_name_opt(&decl.kind)
}
fn kind_name_opt(kind: &Option<v2::decl::Kind>) -> &'static str {
use v2::decl::Kind;
match kind {
Some(Kind::TypeDef(_)) => "type",
Some(Kind::ConstDef(_)) => "const",
Some(Kind::StructDef(_)) => "struct",
Some(Kind::EnumDef(_)) => "enum",
Some(Kind::EnumSetDef(_)) => "enum set",
Some(Kind::UnionDef(_)) => "union",
Some(Kind::SignalDef(_)) => "signal",
Some(Kind::EventDef(_)) => "event",
Some(Kind::CommandDef(_)) => "command",
Some(Kind::QueryDef(_)) => "query",
Some(Kind::FixedDef(_)) => "fixed",
Some(Kind::ReservedSlot(_)) => "reserved",
None => "declaration",
}
}
fn kind_discriminant(kind: &Option<v2::decl::Kind>) -> u8 {
use v2::decl::Kind;
match kind {
None => 0,
Some(Kind::TypeDef(_)) => 1,
Some(Kind::ConstDef(_)) => 2,
Some(Kind::StructDef(_)) => 3,
Some(Kind::EnumDef(_)) => 4,
Some(Kind::EnumSetDef(_)) => 5,
Some(Kind::UnionDef(_)) => 6,
Some(Kind::SignalDef(_)) => 7,
Some(Kind::EventDef(_)) => 8,
Some(Kind::CommandDef(_)) => 9,
Some(Kind::QueryDef(_)) => 10,
Some(Kind::FixedDef(_)) => 11,
Some(Kind::ReservedSlot(_)) => 12,
}
}
fn interaction_desc(decl: &v2::Decl) -> String {
format!("{} {}", kind_name_opt(&decl.kind), decl.name)
}
fn form_desc(inline: bool) -> &'static str {
if inline {
"inline shape"
} else {
"interface list"
}
}
fn timing_str(timing: Option<&v2::Timing>) -> String {
let Some(timing) = timing else {
return "(none)".to_string();
};
let mode = match v2::TimingMode::try_from(timing.mode) {
Ok(v2::TimingMode::StrictPeriodic) => "strict",
Ok(v2::TimingMode::Range) => "range",
_ => "unspecified",
};
let min = timing.min_us.as_deref().unwrap_or("_");
let max = timing.max_us.as_deref().unwrap_or("_");
let default = if timing.default_applied {
" (default)"
} else {
""
};
format!("{mode} [{min}us..{max}us]{default}")
}
fn params_str(params: &[v2::Param]) -> String {
let rendered: Vec<String> = params
.iter()
.map(|param| {
format!(
"{}: {}",
param.name,
field_type_opt_str(param.r#type.as_ref())
)
})
.collect();
format!("({})", rendered.join(", "))
}
fn contracts_str(contracts: &[v2::Contract]) -> String {
let rendered: Vec<String> = contracts
.iter()
.map(|contract| {
let kind = match v2::ContractKind::try_from(contract.kind) {
Ok(v2::ContractKind::Require) => "require",
Ok(v2::ContractKind::Ensure) => "ensure",
_ => "clause",
};
format!("{kind} {}", contract.source)
})
.collect();
format!("[{}]", rendered.join("; "))
}
fn return_str(iface: &str, ordinal: u32, return_type: Option<&v2::ReturnType>) -> String {
let Some(return_type) = return_type else {
return "()".to_string();
};
match &return_type.kind {
Some(v2::return_type::Kind::Value(value)) => field_type_str(value),
Some(v2::return_type::Kind::Fallible(fallible)) => format!(
"{} | {} (transport identity {})",
fallible.ok,
fallible.err,
v2::fallible_transport_identity(iface, ordinal, fallible)
),
None => "()".to_string(),
}
}
fn field_type_opt_str(field_type: Option<&v2::FieldType>) -> String {
field_type
.map(field_type_str)
.unwrap_or_else(|| "()".to_string())
}
fn field_type_str(field_type: &v2::FieldType) -> String {
use v2::field_type::Kind;
let mut rendered = match &field_type.kind {
Some(Kind::Named(name)) => name.clone(),
Some(Kind::Primitive(primitive)) => primitive_name(*primitive).to_string(),
Some(Kind::InlineScalar(_)) => "<inline scalar>".to_string(),
Some(Kind::Tuple(tuple)) => {
let fields: Vec<String> = tuple
.fields
.iter()
.map(|field| {
format!(
"{}: {}",
field.name,
field_type_opt_str(field.r#type.as_ref())
)
})
.collect();
format!("({})", fields.join(", "))
}
Some(Kind::Array(array)) => format!(
"[{}; {}..{}]",
field_type_opt_str(array.element.as_deref()),
array.min,
array.max
),
Some(Kind::Map(map)) => format!(
"{{{}: {}}}",
field_type_opt_str(map.key.as_deref()),
field_type_opt_str(map.value.as_deref())
),
Some(Kind::Stream(stream)) => match &stream.element {
Some(v2::stream_type::Element::Named(name)) => format!("<{name}>"),
Some(v2::stream_type::Element::Primitive(primitive)) => {
format!("<{}>", primitive_name(*primitive))
}
None => "<>".to_string(),
},
None => "?".to_string(),
};
if field_type.optional {
rendered.push('?');
}
rendered
}
fn primitive_name(primitive: i32) -> &'static str {
match v2::PrimitiveType::try_from(primitive) {
Ok(v2::PrimitiveType::Boolean) => "boolean",
Ok(v2::PrimitiveType::Integer) => "integer",
Ok(v2::PrimitiveType::Float) => "float",
Ok(v2::PrimitiveType::String) => "string",
Ok(v2::PrimitiveType::Bytes) => "bytes",
_ => "unspecified",
}
}
fn const_str(def: &v2::ConstDef) -> String {
if let Some(regex) = &def.regex {
return format!("regex {regex}");
}
match &def.type_ref {
Some(type_ref) => format!("{}: {}", type_ref, def.value),
None => def.value.clone(),
}
}
fn type_repr_str(def: &v2::TypeDef) -> String {
let backing = match def
.backing
.as_ref()
.and_then(|backing| backing.kind.as_ref())
{
Some(v2::backing::Kind::Primitive(primitive)) => primitive_name(*primitive).to_string(),
Some(v2::backing::Kind::Unit(unit)) => unit.clone(),
None => "?".to_string(),
};
match &def.width {
Some(v2::type_def::Width::IntWidth(width)) => {
format!("{backing} {}", int_width_name(*width))
}
Some(v2::type_def::Width::FloatWidth(width)) => {
format!("{backing} {}", float_width_name(*width))
}
None => backing,
}
}
fn int_width_name(width: i32) -> &'static str {
match v2::IntWidth::try_from(width) {
Ok(v2::IntWidth::U8) => "u8",
Ok(v2::IntWidth::I8) => "i8",
Ok(v2::IntWidth::U16) => "u16",
Ok(v2::IntWidth::I16) => "i16",
Ok(v2::IntWidth::U32) => "u32",
Ok(v2::IntWidth::I32) => "i32",
Ok(v2::IntWidth::U64) => "u64",
Ok(v2::IntWidth::I64) => "i64",
_ => "unspecified",
}
}
fn float_width_name(width: i32) -> &'static str {
match v2::FloatWidth::try_from(width) {
Ok(v2::FloatWidth::F32) => "f32",
Ok(v2::FloatWidth::F64) => "f64",
_ => "unspecified",
}
}
fn constraint_str(constraint: Option<&v2::Constraint>) -> String {
let Some(constraint) = constraint else {
return "(none)".to_string();
};
let mut parts = Vec::new();
if let Some(min) = &constraint.min {
parts.push(format!("min={min}"));
}
if let Some(max) = &constraint.max {
parts.push(format!("max={max}"));
}
if let Some(step) = &constraint.step {
parts.push(format!("step={step}"));
}
if let Some(len_min) = constraint.len_min {
parts.push(format!("len_min={len_min}"));
}
if let Some(len_max) = constraint.len_max {
parts.push(format!("len_max={len_max}"));
}
if let Some(pattern) = &constraint.pattern {
parts.push(format!("pattern={pattern}"));
}
format!("[{}]", parts.join(" "))
}
fn type_init_str(def: &v2::TypeDef) -> String {
if let Some(declared) = &def.declared_init {
return declared.clone();
}
match def.init.as_ref().and_then(|init| init.value.as_ref()) {
Some(value) => value.clone(),
None => "(none)".to_string(),
}
}
fn signal_init_str(def: &v2::SignalDef) -> String {
if let Some(declared) = &def.declared_init {
return declared.clone();
}
match def.init.as_ref().and_then(|init| init.value.as_ref()) {
Some(value) => value.clone(),
None => "(none)".to_string(),
}
}