use std::collections::{BTreeMap, BTreeSet};
use ridl_ir::{projection, v2};
use crate::classify::{struct_slots, union_slots, value_slots};
use crate::{Category, Change, Verdict, emit, frozen};
pub(crate) fn walk_packages<'a>(
old: &'a v2::Package,
new: &'a v2::Package,
matching: &Matching<'a>,
changes: &mut Vec<Change>,
) {
let pkg = new.name.as_str();
diff_decls(pkg, &old.decls, &new.decls, changes);
diff_interfaces(pkg, old, new, matching, 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))) => {
if a != b {
diff_composite(
&path,
struct_slots(a),
struct_slots(b),
"ordinal",
|| struct_ignoring_order(a) != struct_ignoring_order(b),
changes,
);
}
}
(Some(Kind::EnumDef(a)), Some(Kind::EnumDef(b))) => {
emit_openness(changes, &path, a.open, b.open);
let (a, b) = (
v2::EnumDef {
open: false,
..a.clone()
},
v2::EnumDef {
open: false,
..b.clone()
},
);
if a != b {
let (old_sorted, new_sorted) = (enum_ignoring_order(&a), enum_ignoring_order(&b));
if old_sorted == new_sorted {
emit(changes, path, Category::EnumReordered, None, None);
} else {
diff_composite(
&path,
value_slots(&a.values),
value_slots(&b.values),
"value",
|| {
let old_rest = v2::EnumDef {
values: without_numbers(old_sorted.values),
..old_sorted
};
let new_rest = v2::EnumDef {
values: without_numbers(new_sorted.values),
..new_sorted
};
old_rest != new_rest
},
changes,
);
}
}
}
(Some(Kind::EnumSetDef(a)), Some(Kind::EnumSetDef(b))) => {
emit_openness(changes, &path, a.open, b.open);
let (a, b) = (
v2::EnumSetDef {
open: false,
..a.clone()
},
v2::EnumSetDef {
open: false,
..b.clone()
},
);
if a != b {
let (old_sorted, new_sorted) =
(enum_set_ignoring_order(&a), enum_set_ignoring_order(&b));
if old_sorted == new_sorted {
emit(changes, path, Category::EnumReordered, None, None);
} else {
diff_composite(
&path,
value_slots(&a.bits),
value_slots(&b.bits),
"bit",
|| {
let old_rest = v2::EnumSetDef {
bits: without_numbers(old_sorted.bits),
..old_sorted
};
let new_rest = v2::EnumSetDef {
bits: without_numbers(new_sorted.bits),
..new_sorted
};
old_rest != new_rest
},
changes,
);
}
}
}
(Some(Kind::UnionDef(a)), Some(Kind::UnionDef(b))) => {
if a != b {
diff_composite(
&path,
union_slots(a),
union_slots(b),
"ordinal",
|| 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 emit_openness(changes: &mut Vec<Change>, path: &str, old_open: bool, new_open: bool) {
let category = match (old_open, new_open) {
(false, true) => Category::EnumOpened,
(true, false) => Category::EnumClosed,
_ => return,
};
emit(changes, path.to_string(), category, None, None);
}
fn diff_composite(
path: &str,
old: Vec<(String, i64)>,
new: Vec<(String, i64)>,
slot: &str,
differs_beyond_slots: impl FnOnce() -> bool,
changes: &mut Vec<Change>,
) {
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 || differs_beyond_slots() {
emit(
changes,
path.to_string(),
Category::ConstraintChanged,
None,
None,
);
}
}
type ShapeKey<'a> = (bool, &'a str);
type ShapeAddress<'a> = (&'a str, ShapeKey<'a>);
type Held<'a> = (&'a v2::Package, v2::InterfaceShape<'a>);
fn shape_key<'a>(shape: &v2::InterfaceShape<'a>) -> ShapeKey<'a> {
(shape.is_inline(), shape.name)
}
fn shapes_by_key(package: &v2::Package) -> BTreeMap<ShapeKey<'_>, v2::InterfaceShape<'_>> {
package
.shapes()
.map(|shape| (shape_key(&shape), shape))
.collect()
}
pub(crate) struct Matching<'a> {
pairs: BTreeMap<ShapeAddress<'a>, Held<'a>>,
taken: BTreeSet<ShapeAddress<'a>>,
retired: BTreeMap<&'a str, BTreeSet<u32>>,
}
impl<'a> Matching<'a> {
pub(crate) fn new(old: &[&'a v2::Package], new: &[&'a v2::Package]) -> Self {
let mut by_number: BTreeMap<(&str, u32), Held<'a>> = BTreeMap::new();
let mut retired: BTreeMap<&str, BTreeSet<u32>> = BTreeMap::new();
for package in by_name(new) {
let unit = v2::unit_of(package);
for shape in shapes_by_key(package).into_values() {
if frozen(shape.interface) {
by_number
.entry((unit, shape.interface.number))
.or_insert((package, shape));
}
}
retired
.entry(unit)
.or_default()
.extend(package.retired.iter().map(|entry| entry.number));
}
let mut pairs = BTreeMap::new();
let mut taken = BTreeSet::new();
for package in by_name(old) {
let unit = v2::unit_of(package);
for (key, shape) in shapes_by_key(package) {
if !frozen(shape.interface) {
continue;
}
if let Some(&held) = by_number.get(&(unit, shape.interface.number))
&& taken.insert((held.0.name.as_str(), shape_key(&held.1)))
{
pairs.insert((package.name.as_str(), key), held);
}
}
}
Self {
pairs,
taken,
retired,
}
}
fn retires(&self, unit: &str, number: u32) -> bool {
self.retired
.get(unit)
.is_some_and(|numbers| numbers.contains(&number))
}
}
fn by_name<'a>(set: &[&'a v2::Package]) -> Vec<&'a v2::Package> {
let mut packages: Vec<&v2::Package> = set.to_vec();
packages.sort_by_key(|package| package.name.as_str());
packages
}
fn diff_interfaces<'a>(
pkg: &str,
old: &'a v2::Package,
new: &'a v2::Package,
matching: &Matching<'a>,
changes: &mut Vec<Change>,
) {
let old_shapes = shapes_by_key(old);
let new_shapes = shapes_by_key(new);
let mut pairs: BTreeMap<ShapeKey<'_>, Held<'_>> = BTreeMap::new();
let mut taken: BTreeSet<ShapeKey<'_>> = new_shapes
.keys()
.copied()
.filter(|key| matching.taken.contains(&(new.name.as_str(), *key)))
.collect();
for (old_key, old_shape) in &old_shapes {
if frozen(old_shape.interface) {
if let Some(held) = matching.pairs.get(&(old.name.as_str(), *old_key)) {
pairs.insert(*old_key, *held);
}
} else if let Some(new_shape) = new_shapes.get(old_key)
&& taken.insert(*old_key)
{
pairs.insert(*old_key, (new, *new_shape));
}
}
for (old_key, old_shape) in &old_shapes {
let Some((new_package, new_shape)) = pairs.get(old_key) else {
let number = old_shape.interface.number;
let sanctioned =
frozen(old_shape.interface) && matching.retires(v2::unit_of(old), 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 held_by = new_package.name.as_str();
let name = new_shape.name;
let old_catalog_name = old.catalog_name(old_shape);
let new_catalog_name = new_package.catalog_name(new_shape);
if old_catalog_name != new_catalog_name {
emit(
changes,
format!("{held_by}/{name}"),
Category::InterfaceRenamed,
Some(old_catalog_name),
Some(new_catalog_name),
);
}
emit_number(
changes,
format!("{held_by}/{name}"),
old_shape.interface,
new_shape.interface,
);
if !new_shape.is_inline() {
if interface_envelope_differs(old_shape.interface, new_shape.interface) {
emit(
changes,
format!("{held_by}/{name}"),
Category::DocOnly,
None,
None,
);
}
emit_visibility(
changes,
format!("{held_by}/{name}"),
old_shape.visibility(),
new_shape.visibility(),
);
}
diff_interface(
held_by,
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 emit_number(changes: &mut Vec<Change>, path: String, old: &v2::Interface, new: &v2::Interface) {
if old.number == 0 || new.number == 0 {
return;
}
let category = if old.number != new.number {
Category::InterfaceNumberChanged
} else if old.provisional && !new.provisional {
Category::InterfaceFrozen
} else {
return;
};
emit(
changes,
path,
category,
Some(number_detail(old)),
Some(number_detail(new)),
);
}
fn number_detail(interface: &v2::Interface) -> String {
if interface.provisional {
format!("{} (provisional)", interface.number)
} else {
interface.number.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);
if old.limit != new.limit && kind_discriminant(&old.kind) == kind_discriminant(&new.kind) {
emit(
changes,
path.to_string(),
Category::LimitChanged,
Some(limit_str(old.limit)),
Some(limit_str(new.limit)),
);
}
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.family != b.family {
emit(
changes,
path.to_string(),
Category::InteractionClassChanged,
Some(class_str(a.family, 0)),
Some(class_str(b.family, 0)),
);
}
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.family != b.family {
emit(
changes,
path.to_string(),
Category::InteractionClassChanged,
Some(class_str(a.family, 0)),
Some(class_str(b.family, 0)),
);
}
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.family != b.family || a.shape != b.shape {
emit(
changes,
path.to_string(),
Category::InteractionClassChanged,
Some(class_str(a.family, a.shape)),
Some(class_str(b.family, b.shape)),
);
}
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.family != b.family {
emit(
changes,
path.to_string(),
Category::InteractionClassChanged,
Some(class_str(a.family, 0)),
Some(class_str(b.family, 0)),
);
}
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 without_numbers(mut values: Vec<v2::EnumValue>) -> Vec<v2::EnumValue> {
for value in &mut values {
value.value = 0;
}
values
}
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 class_str(family: i32, shape: i32) -> String {
let family = v2::Family::try_from(family).map_or_else(
|_| family.to_string(),
|family| {
family
.as_str_name()
.trim_start_matches("FAMILY_")
.to_ascii_lowercase()
},
);
match v2::Shape::try_from(shape) {
Ok(v2::Shape::None) => family,
Ok(shape) => format!(
"{family} {}",
shape
.as_str_name()
.trim_start_matches("SHAPE_")
.to_ascii_lowercase()
),
Err(_) => format!("{family} {shape}"),
}
}
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 limit_str(limit: Option<u32>) -> String {
limit.map_or_else(|| "none".to_string(), |bytes| bytes.to_string())
}
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,
}
}
pub(crate) 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(),
}
}
pub(crate) fn podbuf_inline_appends(
pair: &[Change],
holders: &[&v2::Package],
old: &[&v2::Package],
new: &[&v2::Package],
) -> Vec<Change> {
struct Held {
uses: String,
old_size: Option<u32>,
new_head: Option<projection::podbuf::Head>,
}
let mut held: BTreeMap<(&str, &str), Option<Held>> = BTreeMap::new();
let mut changes = Vec::new();
for change in pair {
if change.category != Category::DeclAdded || change.verdict != Verdict::Compatible {
continue;
}
let mut segments = change.path.split('/');
let (Some(package), Some(name), Some(field), None) = (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) else {
continue;
};
let Some(new_def) = struct_def(new, package, name) else {
continue;
};
let entry = held.entry((package, name)).or_insert_with(|| {
let uses = inline_uses(old, holders, package, name);
if uses.is_empty() {
return None;
}
let old_def = struct_def(old, package, name)?;
Some(Held {
uses: uses.join(", "),
old_size: head(old, package, name, old_def).map(|head| head.size),
new_head: head(new, package, name, new_def),
})
});
let Some(Held {
uses,
old_size,
new_head,
}) = entry
else {
continue;
};
let past_old_head = match (old_size, new_head) {
(Some(old_size), Some(head)) => head.fields.iter().any(|placement| {
matches!(&placement.source, projection::podbuf::Source::Field { name, .. } if name == field)
&& placement.offset + placement.size > *old_size
}),
_ => true,
};
if !past_old_head {
continue;
}
emit(
&mut changes,
change.path.clone(),
Category::PodbufInlineAppend,
None,
Some(format!("nested inline in {uses}")),
);
}
changes
}
fn struct_def<'p>(
scope: &[&'p v2::Package],
package: &str,
name: &str,
) -> Option<&'p v2::StructDef> {
let home = scope.iter().copied().find(|pkg| pkg.name == package)?;
match &crate::classify::find_decl(home, name)?.kind {
Some(v2::decl::Kind::StructDef(def)) => Some(def),
_ => None,
}
}
fn others_of<'p>(scope: &[&'p v2::Package], index: usize) -> Vec<&'p v2::Package> {
scope
.iter()
.enumerate()
.filter(|(other, _)| *other != index)
.map(|(_, pkg)| *pkg)
.collect()
}
fn head(
scope: &[&v2::Package],
package: &str,
name: &str,
def: &v2::StructDef,
) -> Option<projection::podbuf::Head> {
let index = scope.iter().position(|pkg| pkg.name == package)?;
let others = others_of(scope, index);
let packages = ridl_ir::projection::flatbuffers::Packages {
package: scope[index],
others: &others,
};
projection::podbuf::struct_head(packages, name, def).ok()
}
fn compatible_member_appends<'c>(
pair: &'c [Change],
new: &v2::Package,
) -> impl Iterator<Item = (&'c str, &'c str)> {
let package_name = new.name.clone();
pair.iter().filter_map(move |change| {
if change.category != Category::DeclAdded || change.verdict != Verdict::Compatible {
return None;
}
let mut segments = change.path.split('/');
let (Some(package), Some(name), Some(member), None) = (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) else {
return None;
};
(package == package_name).then_some((name, member))
})
}
pub(crate) fn podbuf_width_changes(
pair: &[Change],
old: &v2::Package,
new: &v2::Package,
) -> Vec<Change> {
use ridl_ir::projection::podbuf::{enum_width, union_tag_width};
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut changes = Vec::new();
for (name, _) in compatible_member_appends(pair, new) {
if !seen.insert(name) {
continue;
}
fn find<'p>(pkg: &'p v2::Package, name: &str) -> Option<&'p v2::Decl> {
pkg.decls.iter().find(|decl| decl.name == name)
}
let (Some(old_decl), Some(new_decl)) = (find(old, name), find(new, name)) else {
continue;
};
let widths = match (&old_decl.kind, &new_decl.kind) {
(Some(v2::decl::Kind::EnumDef(old_def)), Some(v2::decl::Kind::EnumDef(new_def))) => {
(enum_width(old_def), enum_width(new_def))
}
(
Some(v2::decl::Kind::EnumSetDef(old_def)),
Some(v2::decl::Kind::EnumSetDef(new_def)),
) => ((old_def.width(), false), (new_def.width(), false)),
(Some(v2::decl::Kind::UnionDef(old_def)), Some(v2::decl::Kind::UnionDef(new_def))) => (
(union_tag_width(old_def), false),
(union_tag_width(new_def), false),
),
_ => continue,
};
if widths.0 == widths.1 {
continue;
}
emit(
&mut changes,
format!("{}/{name}", new.name),
Category::PodbufWidthChange,
Some(int_width_name(widths.0.0 as i32).to_string()),
Some(int_width_name(widths.1.0 as i32).to_string()),
);
}
changes
}
pub(crate) fn flatbuffers_width_changes(
pair: &[Change],
old: &v2::Package,
new: &v2::Package,
) -> Vec<Change> {
use ridl_ir::projection::flatbuffers::{enum_integer, enum_set_integer};
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut changes = Vec::new();
for (name, _) in compatible_member_appends(pair, new) {
if !seen.insert(name) {
continue;
}
fn find<'p>(pkg: &'p v2::Package, name: &str) -> Option<&'p v2::Decl> {
pkg.decls.iter().find(|decl| decl.name == name)
}
let (Some(old_decl), Some(new_decl)) = (find(old, name), find(new, name)) else {
continue;
};
let widths = match (&old_decl.kind, &new_decl.kind) {
(Some(v2::decl::Kind::EnumDef(old_def)), Some(v2::decl::Kind::EnumDef(new_def))) => {
(enum_integer(old_def), enum_integer(new_def))
}
(
Some(v2::decl::Kind::EnumSetDef(old_def)),
Some(v2::decl::Kind::EnumSetDef(new_def)),
) => (enum_set_integer(old_def), enum_set_integer(new_def)),
_ => continue,
};
if widths.0 == widths.1 {
continue;
}
emit(
&mut changes,
format!("{}/{name}", new.name),
Category::FlatbuffersWidthChange,
Some(int_width_name(widths.0 as i32).to_string()),
Some(int_width_name(widths.1 as i32).to_string()),
);
}
changes
}
pub(crate) fn proto3_width_changes(
pair: &[Change],
old: &v2::Package,
new: &v2::Package,
) -> Vec<Change> {
use ridl_ir::projection::proto3::enum_set_scalar;
let mut seen: BTreeSet<&str> = BTreeSet::new();
let mut changes = Vec::new();
for (name, _) in compatible_member_appends(pair, new) {
if !seen.insert(name) {
continue;
}
fn find<'p>(pkg: &'p v2::Package, name: &str) -> Option<&'p v2::Decl> {
pkg.decls.iter().find(|decl| decl.name == name)
}
let (Some(old_decl), Some(new_decl)) = (find(old, name), find(new, name)) else {
continue;
};
let widths = match (&old_decl.kind, &new_decl.kind) {
(
Some(v2::decl::Kind::EnumSetDef(old_def)),
Some(v2::decl::Kind::EnumSetDef(new_def)),
) => (
enum_set_scalar(old_def.width),
enum_set_scalar(new_def.width),
),
_ => continue,
};
if widths.0 == widths.1 {
continue;
}
emit(
&mut changes,
format!("{}/{name}", new.name),
Category::Proto3WidthChange,
Some(widths.0.as_str().to_string()),
Some(widths.1.as_str().to_string()),
);
}
changes
}
pub(crate) fn fold_strict_enum_appends(
pair: Vec<Change>,
old: &v2::Package,
new: &v2::Package,
) -> Vec<Change> {
let strict_member = |change: &Change| -> Option<(String, String)> {
if change.category != Category::DeclAdded {
return None;
}
let mut segments = change.path.split('/');
let (Some(package), Some(name), Some(member), None) = (
segments.next(),
segments.next(),
segments.next(),
segments.next(),
) else {
return None;
};
if package != new.name {
return None;
}
fn find<'p>(pkg: &'p v2::Package, name: &str) -> Option<&'p v2::Decl> {
pkg.decls.iter().find(|decl| decl.name == name)
}
crate::classify::is_strict_enum_append(find(old, name)?, find(new, name)?)
.then(|| (name.to_string(), member.to_string()))
};
let mut appended: BTreeMap<String, Vec<String>> = BTreeMap::new();
for change in &pair {
if let Some((name, member)) = strict_member(change) {
appended.entry(name).or_default().push(member);
}
}
let mut folded = Vec::with_capacity(pair.len());
for change in pair {
match strict_member(&change) {
None => folded.push(change),
Some((name, _)) => {
if let Some(members) = appended.remove(&name) {
emit(
&mut folded,
format!("{}/{name}", new.name),
Category::StrictEnumAppend,
None,
Some(members.join(", ")),
);
}
}
}
}
folded
}
fn inline_uses(
scope: &[&v2::Package],
holders: &[&v2::Package],
package: &str,
name: &str,
) -> Vec<String> {
use ridl_ir::projection::flatbuffers::Packages;
use ridl_ir::projection::podbuf::{field_form, root_shape_holds_inline, shape_holds_inline};
let target = projection::podbuf::Inlined::Declaration {
package: package.to_string(),
name: name.to_string(),
};
let mut uses = Vec::new();
for home in holders {
let index = scope
.iter()
.position(|pkg| pkg.name == home.name)
.expect("every holder is in the scope");
let others = others_of(scope, index);
let packages = Packages {
package: home,
others: &others,
};
let holds = |ty: &v2::FieldType, fixed: bool| match field_form(packages, ty) {
Ok(form) => holds_inline(&form, &target, fixed),
Err(_) => shape_holds_inline(packages, ty, package, name),
};
let root_holds = |ty: &v2::FieldType| match field_form(packages, ty) {
Ok(form) => root_holds_inline(&form, &target),
Err(_) => root_shape_holds_inline(packages, ty, package, name),
};
for decl in &home.decls {
let Some(v2::decl::Kind::StructDef(def)) = &decl.kind else {
continue;
};
for slot in &def.members {
let Some(v2::struct_member::Member::Field(field)) = &slot.member else {
continue;
};
if field.r#type.as_ref().is_some_and(|ty| holds(ty, true)) {
uses.push(format!("{}/{}/{}", home.name, decl.name, field.name));
}
}
}
for shape in home.shapes() {
for decl in &shape.interface.interactions {
if interaction_payloads(decl).iter().any(root_holds) {
uses.push(format!("{}/{}/{}", home.name, shape.name, decl.name));
}
}
}
}
uses.sort();
uses
}
pub(crate) fn interaction_payloads(decl: &v2::Decl) -> Vec<v2::FieldType> {
use v2::decl::Kind;
fn named(reference: &str) -> v2::FieldType {
v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Named(reference.to_string())),
}
}
fn request(params: &[v2::Param]) -> Option<v2::FieldType> {
use projection::size::PayloadShape;
let induced = ridl_ir::codegen::params_as_tuple(params);
match ridl_ir::codegen::request_shape(params, induced.as_ref()) {
PayloadShape::Named(reference) => Some(named(reference)),
PayloadShape::Tuple(tuple) => Some(v2::FieldType {
optional: false,
kind: Some(v2::field_type::Kind::Tuple(tuple.clone())),
}),
PayloadShape::Params([param]) => param
.r#type
.as_ref()
.filter(|ty| matches!(ty.kind, Some(v2::field_type::Kind::Named(_))))
.cloned(),
_ => None,
}
}
let mut payloads = Vec::new();
match &decl.kind {
Some(Kind::CommandDef(def)) => payloads.extend(request(&def.params)),
Some(Kind::QueryDef(def)) => {
payloads.extend(request(&def.params));
match def.return_type.as_ref().and_then(|ret| ret.kind.as_ref()) {
Some(v2::return_type::Kind::Value(ty)) => payloads.push(ty.clone()),
Some(v2::return_type::Kind::Fallible(fallible)) => {
payloads.push(named(&fallible.ok));
payloads.push(named(&fallible.err));
}
None => {}
}
}
Some(Kind::FixedDef(def)) => payloads.extend(def.payload.clone()),
_ => {}
}
payloads
}
pub(crate) fn root_holds_inline(
form: &projection::podbuf::Form,
target: &projection::podbuf::Inlined,
) -> bool {
match form {
projection::podbuf::Form::InlineArray { element, .. } => root_holds_inline(element, target),
_ => holds_inline(form, target, false),
}
}
pub(crate) fn holds_inline(
form: &projection::podbuf::Form,
target: &projection::podbuf::Inlined,
fixed: bool,
) -> bool {
use projection::podbuf::{DescriptorKind, Form, Head, Inlined};
let any_field = |head: &Head, fixed: bool| {
head.fields
.iter()
.any(|placement| holds_inline(&placement.form, target, fixed))
};
match form {
Form::Inline { of, .. } if of == target => fixed,
Form::Inline {
of: Inlined::Declaration { .. },
..
} => false,
Form::Inline {
of: Inlined::Tuple,
head,
} => any_field(head, true),
Form::InlineArray { element, .. } => holds_inline(element, target, true),
Form::Descriptor(DescriptorKind::Array { element, .. }) => {
holds_inline(element, target, false)
}
Form::Descriptor(DescriptorKind::Map { entry, .. }) => any_field(entry, false),
Form::Descriptor(
DescriptorKind::Arm { .. } | DescriptorKind::Text { .. } | DescriptorKind::Bytes { .. },
)
| Form::Bool
| Form::Int { .. }
| Form::Float(_)
| Form::Enum { .. }
| Form::EnumSet { .. } => false,
}
}