use std::collections::{BTreeMap, BTreeSet};
use sha2::{Digest, Sha256};
use crate::v2::{
Decl, DocLink, FieldType, Package, Param, TypeDef, decl, field_type, return_type, stream_type,
struct_member,
};
pub fn reachable_decls<'a>(
package: &'a Package,
others: &[&'a Package],
) -> BTreeMap<String, &'a Decl> {
let index = Index::new(package, others);
index
.closure()
.into_iter()
.map(|(canonical, (_, decl))| (canonical, decl))
.collect()
}
pub fn reduced_package(package: &Package, others: &[&Package]) -> Package {
let index = Index::new(package, others);
let mut reduced = Package {
name: package.name.clone(),
decls: index
.closure()
.into_iter()
.map(|(canonical, (owner, decl))| {
let mut decl = decl.clone();
decl.name = canonical;
visit_refs(&mut decl, &mut |name| index.canonicalize(name, owner));
blank_docs(&mut decl);
decl
})
.collect(),
interfaces: package
.shapes()
.map(|shape| {
let mut interface = shape.interface.clone();
interface.name = shape.name.to_owned();
interface.visibility = shape.visibility();
interface
})
.collect(),
services: vec![],
retired: vec![],
};
for interface in &mut reduced.interfaces {
interface.doc.clear();
interface.labels.clear();
interface.deprecated = None;
interface.links.clear();
interface.see.clear();
interface.since.clear();
for interaction in &mut interface.interactions {
visit_refs(interaction, &mut |name| index.canonicalize(name, ROOT));
blank_docs(interaction);
}
}
reduced
.interfaces
.sort_by(|a, b| (a.number, &a.name).cmp(&(b.number, &b.name)));
reduced
}
pub fn catalog_hash(package: &Package, others: &[&Package]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(crate::v2::to_binary(&reduced_package(package, others)));
hasher.finalize().into()
}
const ROOT: usize = 0;
struct Index<'a> {
packages: Vec<&'a Package>,
bare: Vec<BTreeMap<&'a str, &'a Decl>>,
qualified: BTreeMap<String, (usize, &'a Decl)>,
}
impl<'a> Index<'a> {
fn new(package: &'a Package, others: &[&'a Package]) -> Self {
let packages: Vec<&'a Package> = std::iter::once(package)
.chain(
others
.iter()
.copied()
.filter(|other| other.name != package.name),
)
.collect();
let bare = packages
.iter()
.map(|p| p.decls.iter().map(|d| (d.name.as_str(), d)).collect())
.collect();
let mut qualified = BTreeMap::new();
for (i, p) in packages.iter().enumerate() {
for d in &p.decls {
qualified.insert(format!("{}.{}", p.name, d.name), (i, d));
}
}
Self {
packages,
bare,
qualified,
}
}
fn resolve(&self, name: &str, context: usize) -> Option<(String, usize, &'a Decl)> {
if let Some(decl) = self.bare[context].get(name) {
return Some((self.canonical(context, name), context, decl));
}
let (owner, decl) = self.qualified.get(name)?;
Some((self.canonical(*owner, &decl.name), *owner, decl))
}
fn canonical(&self, owner: usize, bare: &str) -> String {
if owner == ROOT {
bare.to_owned()
} else {
format!("{}.{}", self.packages[owner].name, bare)
}
}
fn canonicalize(&self, name: &mut String, context: usize) {
if let Some((canonical, _, _)) = self.resolve(name, context) {
*name = canonical;
}
}
fn closure(&self) -> BTreeMap<String, (usize, &'a Decl)> {
let mut pending: Vec<Pending> = Vec::new();
for shape in self.packages[ROOT].shapes() {
for interaction in &shape.interface.interactions {
collect_refs(interaction, ROOT, &mut pending);
}
}
let mut reached: BTreeMap<String, (usize, &'a Decl)> = BTreeMap::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
while let Some((name, context, origin)) = pending.pop() {
let found = match origin {
Origin::TypeRef => self.resolve(&name, context).into_iter().collect(),
Origin::Expr => self.resolve_expr(&name, context),
};
for (canonical, owner, decl) in found {
if !seen.insert(canonical.clone()) {
continue;
}
reached.insert(canonical, (owner, decl));
collect_refs(decl, owner, &mut pending);
}
}
reached
}
fn resolve_expr(&self, name: &str, context: usize) -> Vec<(String, usize, &'a Decl)> {
if let Some(found) = self.resolve(name, context) {
return vec![found];
}
if name.contains('.') {
return vec![];
}
self.bare
.iter()
.enumerate()
.filter_map(|(owner, decls)| {
let decl = decls.get(name)?;
Some((self.canonical(owner, name), owner, *decl))
})
.collect()
}
}
type Pending = (String, usize, Origin);
#[derive(Clone, Copy)]
enum Origin {
TypeRef,
Expr,
}
fn collect_refs(decl: &Decl, context: usize, out: &mut Vec<Pending>) {
let mut copy = decl.clone();
visit_refs(&mut copy, &mut |name| {
out.push((name.clone(), context, Origin::TypeRef));
});
visit_exprs(decl, &mut |source| {
expr_names(source, &mut |name| {
out.push((name.to_owned(), context, Origin::Expr));
});
});
}
fn visit_exprs(decl: &Decl, f: &mut dyn FnMut(&str)) {
match &decl.kind {
Some(decl::Kind::ConstDef(def)) => f(&def.value),
Some(decl::Kind::CommandDef(def)) => {
for contract in &def.contracts {
f(&contract.source);
}
}
Some(decl::Kind::QueryDef(def)) => {
for contract in &def.contracts {
f(&contract.source);
}
}
Some(decl::Kind::TypeDef(_))
| Some(decl::Kind::StructDef(_))
| Some(decl::Kind::EnumDef(_))
| Some(decl::Kind::EnumSetDef(_))
| Some(decl::Kind::UnionDef(_))
| Some(decl::Kind::SignalDef(_))
| Some(decl::Kind::EventDef(_))
| Some(decl::Kind::FixedDef(_))
| Some(decl::Kind::ReservedSlot(_))
| None => {}
}
}
fn expr_names(source: &str, f: &mut dyn FnMut(&str)) {
let bytes = source.as_bytes();
let is_start = |b: u8| b.is_ascii_alphabetic() || b == b'_';
let is_part = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b == b'"' {
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
i += if bytes[i] == b'\\' { 2 } else { 1 };
}
i += 1;
} else if b.is_ascii_digit() {
while i < bytes.len()
&& (is_part(bytes[i])
|| (bytes[i] == b'.' && bytes.get(i + 1).is_some_and(u8::is_ascii_digit)))
{
i += 1;
}
} else if is_start(b) {
let start = i;
loop {
while i < bytes.len() && is_part(bytes[i]) {
i += 1;
}
f(&source[start..i]);
if bytes.get(i) == Some(&b'.') && bytes.get(i + 1).is_some_and(|&b| is_start(b)) {
i += 1;
} else {
break;
}
}
} else {
i += 1;
}
}
}
fn visit_refs(decl: &mut Decl, f: &mut dyn FnMut(&mut String)) {
match &mut decl.kind {
Some(decl::Kind::TypeDef(def)) => visit_type_def(def, f),
Some(decl::Kind::ConstDef(def)) => {
if let Some(name) = &mut def.type_ref {
f(name);
}
}
Some(decl::Kind::StructDef(def)) => {
for member in &mut def.members {
if let Some(struct_member::Member::Field(field)) = &mut member.member
&& let Some(ty) = &mut field.r#type
{
visit_field_type(ty, f);
}
}
}
Some(decl::Kind::EnumDef(_)) | Some(decl::Kind::ReservedSlot(_)) | None => {}
Some(decl::Kind::EnumSetDef(def)) => {
if let Some(name) = &mut def.backing_enum {
f(name);
}
}
Some(decl::Kind::UnionDef(def)) => {
for arm in &mut def.arms {
f(&mut arm.type_ref);
}
}
Some(decl::Kind::SignalDef(def)) => f(&mut def.payload),
Some(decl::Kind::EventDef(def)) => f(&mut def.payload),
Some(decl::Kind::CommandDef(def)) => {
for param in &mut def.params {
if let Some(ty) = &mut param.r#type {
visit_field_type(ty, f);
}
}
}
Some(decl::Kind::QueryDef(def)) => {
for param in &mut def.params {
if let Some(ty) = &mut param.r#type {
visit_field_type(ty, f);
}
}
match def.return_type.as_mut().and_then(|r| r.kind.as_mut()) {
Some(return_type::Kind::Value(ty)) => visit_field_type(ty, f),
Some(return_type::Kind::Fallible(fallible)) => {
f(&mut fallible.ok);
f(&mut fallible.err);
}
None => {}
}
}
Some(decl::Kind::FixedDef(def)) => {
if let Some(ty) = &mut def.payload {
visit_field_type(ty, f);
}
}
}
}
fn visit_type_def(def: &mut TypeDef, f: &mut dyn FnMut(&mut String)) {
if let Some(constant) = def
.constraint
.as_mut()
.and_then(|c| c.pattern_const.as_mut())
{
f(constant);
}
}
fn visit_field_type(ty: &mut FieldType, f: &mut dyn FnMut(&mut String)) {
match &mut ty.kind {
Some(field_type::Kind::Named(name)) => f(name),
Some(field_type::Kind::Primitive(_)) | None => {}
Some(field_type::Kind::InlineScalar(def)) => visit_type_def(def, f),
Some(field_type::Kind::Tuple(tuple)) => {
for field in &mut tuple.fields {
if let Some(ty) = &mut field.r#type {
visit_field_type(ty, f);
}
}
}
Some(field_type::Kind::Array(array)) => {
if let Some(element) = &mut array.element {
visit_field_type(element, f);
}
}
Some(field_type::Kind::Map(map)) => {
for ty in [&mut map.key, &mut map.value].into_iter().flatten() {
visit_field_type(ty, f);
}
}
Some(field_type::Kind::Stream(stream)) => {
if let Some(stream_type::Element::Named(name)) = &mut stream.element {
f(name);
}
}
}
}
fn blank_docs(decl: &mut Decl) {
decl.doc.clear();
decl.labels.clear();
decl.deprecated = None;
blank_doc_links(&mut decl.links, &mut decl.see, &mut decl.since);
match &mut decl.kind {
Some(decl::Kind::StructDef(def)) => {
for member in &mut def.members {
if let Some(struct_member::Member::Field(field)) = &mut member.member {
field.doc.clear();
field.labels.clear();
field.deprecated = None;
blank_doc_links(&mut field.links, &mut field.see, &mut field.since);
}
}
}
Some(decl::Kind::UnionDef(def)) => {
for arm in &mut def.arms {
arm.doc.clear();
blank_doc_links(&mut arm.links, &mut arm.see, &mut arm.since);
}
}
Some(decl::Kind::EnumDef(def)) => {
for value in &mut def.values {
value.doc.clear();
blank_doc_links(&mut value.links, &mut value.see, &mut value.since);
}
}
Some(decl::Kind::EnumSetDef(def)) => {
for bit in &mut def.bits {
bit.doc.clear();
blank_doc_links(&mut bit.links, &mut bit.see, &mut bit.since);
}
}
Some(decl::Kind::CommandDef(def)) => blank_param_docs(&mut def.params),
Some(decl::Kind::QueryDef(def)) => blank_param_docs(&mut def.params),
Some(decl::Kind::TypeDef(_))
| Some(decl::Kind::ConstDef(_))
| Some(decl::Kind::SignalDef(_))
| Some(decl::Kind::EventDef(_))
| Some(decl::Kind::FixedDef(_))
| Some(decl::Kind::ReservedSlot(_))
| None => {}
}
}
fn blank_param_docs(params: &mut [Param]) {
for param in params {
param.doc.clear();
blank_doc_links(&mut param.links, &mut param.see, &mut param.since);
}
}
fn blank_doc_links(links: &mut Vec<DocLink>, see: &mut Vec<DocLink>, since: &mut Vec<String>) {
links.clear();
see.clear();
since.clear();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::v2::{
ArrayType, CommandDef, ConstDef, Constraint, Contract, ContractKind, Decl, DocLink,
EnumDef, EnumSetDef, EnumValue, EventDef, FallibleType, Field, FieldType, FixedDef,
Interface, MapType, Param, QueryDef, RetiredInterface, ReturnType, Service, ServiceShape,
SignalDef, StreamType, StructDef, StructMember, TupleField, TupleType, TypeDef, UnionArm,
UnionDef, Visibility, decl, field_type, return_type, service_shape, stream_type,
struct_member,
};
fn named(name: &str) -> FieldType {
FieldType {
optional: false,
kind: Some(field_type::Kind::Named(name.to_owned())),
}
}
fn struct_decl(name: &str, field_types: &[&str]) -> Decl {
Decl {
name: name.to_owned(),
kind: Some(decl::Kind::StructDef(StructDef {
members: field_types
.iter()
.enumerate()
.map(|(i, ty)| StructMember {
member: Some(struct_member::Member::Field(Box::new(Field {
name: format!("f{i}"),
ordinal: i as u32 + 1,
r#type: Some(named(ty)),
..Default::default()
}))),
})
.collect(),
fixed_layout: false,
})),
..Default::default()
}
}
fn scalar_decl(name: &str) -> Decl {
Decl {
name: name.to_owned(),
kind: Some(decl::Kind::TypeDef(TypeDef::default())),
..Default::default()
}
}
fn signal(name: &str, payload: &str) -> Decl {
Decl {
name: name.to_owned(),
ordinal: 1,
kind: Some(decl::Kind::SignalDef(SignalDef {
payload: payload.to_owned(),
..Default::default()
})),
..Default::default()
}
}
fn inline_service(name: &str, interactions: Vec<Decl>) -> Service {
Service {
name: name.to_owned(),
visibility: Visibility::Public as i32,
shapes: vec![ServiceShape {
kind: Some(service_shape::Kind::Inline(Interface {
interactions,
number: 2,
provisional: true,
..Default::default()
})),
}],
..Default::default()
}
}
fn fixture() -> (Package, Package) {
let p = Package {
name: "p".to_owned(),
decls: vec![
struct_decl("Point", &["Coord", "fw.Unit"]),
scalar_decl("Coord"),
scalar_decl("Unused"),
],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 1,
provisional: true,
..Default::default()
}],
..Default::default()
};
let fw = Package {
name: "fw".to_owned(),
decls: vec![scalar_decl("Unit"), scalar_decl("Other")],
..Default::default()
};
(p, fw)
}
fn foreign_fixture() -> (Package, Package) {
let p = Package {
name: "p".to_owned(),
decls: vec![struct_decl("Point", &["fw.Unit"])],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 1,
provisional: true,
..Default::default()
}],
..Default::default()
};
let fw = Package {
name: "fw".to_owned(),
decls: vec![
struct_decl("Unit", &["Coord"]),
scalar_decl("Coord"),
scalar_decl("Other"),
],
..Default::default()
};
(p, fw)
}
fn hash_of(p: &Package, fw: &Package) -> [u8; 32] {
catalog_hash(p, &[fw])
}
fn field_type_names(decl: &Decl) -> Vec<String> {
let Some(decl::Kind::StructDef(def)) = &decl.kind else {
panic!("{} is not a struct", decl.name);
};
def.members
.iter()
.filter_map(|m| match &m.member {
Some(struct_member::Member::Field(field)) => match &field.r#type.as_ref()?.kind {
Some(field_type::Kind::Named(name)) => Some(name.clone()),
_ => None,
},
_ => None,
})
.collect()
}
#[test]
fn the_closure_reaches_local_and_foreign_types_and_nothing_else() {
let (p, fw) = fixture();
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Coord", "Point", "fw.Unit"]);
}
#[test]
fn the_order_of_the_other_packages_does_not_move_the_hash() {
let p = Package {
name: "p".to_owned(),
decls: vec![struct_decl("Point", &["fa.A", "fb.B"])],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 1,
provisional: true,
..Default::default()
}],
..Default::default()
};
let fa = Package {
name: "fa".to_owned(),
decls: vec![scalar_decl("A")],
..Default::default()
};
let fb = Package {
name: "fb".to_owned(),
decls: vec![scalar_decl("B")],
..Default::default()
};
let reached: Vec<String> = reachable_decls(&p, &[&fa, &fb]).into_keys().collect();
assert_eq!(reached, vec!["Point", "fa.A", "fb.B"]);
assert_eq!(catalog_hash(&p, &[&fa, &fb]), catalog_hash(&p, &[&fb, &fa]));
}
#[test]
fn the_hash_is_stable_across_runs() {
let (p, fw) = fixture();
assert_eq!(hash_of(&p, &fw), hash_of(&p, &fw));
}
#[test]
fn an_unreached_declaration_does_not_move_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.decls.retain(|d| d.name != "Unused");
assert_eq!(hash_of(&p, &fw), before);
}
#[test]
fn a_reached_foreign_type_moves_the_hash() {
let (p, mut fw) = fixture();
let before = hash_of(&p, &fw);
fw.decls[0] = struct_decl("Unit", &[]);
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_bare_name_in_a_foreign_declaration_resolves_in_its_own_package() {
let (p, mut fw) = foreign_fixture();
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Point", "fw.Coord", "fw.Unit"]);
let reduced = reduced_package(&p, &[&fw]);
let unit = reduced.decls.iter().find(|d| d.name == "fw.Unit").unwrap();
assert_eq!(field_type_names(unit), vec!["fw.Coord"]);
let before = hash_of(&p, &fw);
fw.decls[1] = struct_decl("Coord", &[]);
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_bare_name_in_a_foreign_declaration_does_not_pick_the_root_packages_homonym() {
let (mut p, fw) = foreign_fixture();
p.decls.push(scalar_decl("Coord"));
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Point", "fw.Coord", "fw.Unit"]);
let reduced = reduced_package(&p, &[&fw]);
let unit = reduced.decls.iter().find(|d| d.name == "fw.Unit").unwrap();
assert_eq!(field_type_names(unit), vec!["fw.Coord"]);
let before = hash_of(&p, &fw);
p.decls[1] = struct_decl("Coord", &[]);
assert_eq!(hash_of(&p, &fw), before);
}
#[test]
fn a_qualified_reference_to_the_root_package_is_canonical_as_bare() {
let (mut p, mut fw) = foreign_fixture();
p.decls.push(scalar_decl("X"));
p.interfaces[0].interactions.push(signal("x", "p.X"));
fw.decls[0] = struct_decl("Unit", &["p.X"]);
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Point", "X", "fw.Unit"]);
let reduced = reduced_package(&p, &[&fw]);
let unit = reduced.decls.iter().find(|d| d.name == "fw.Unit").unwrap();
assert_eq!(field_type_names(unit), vec!["X"]);
let Some(decl::Kind::SignalDef(def)) = &reduced.interfaces[0].interactions[1].kind else {
panic!("not a signal");
};
assert_eq!(def.payload, "X");
}
#[test]
fn an_unresolved_name_stays_as_written() {
let (mut p, fw) = fixture();
p.decls[0] = struct_decl("Point", &["u32", "nowhere.Missing"]);
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Point"]);
let reduced = reduced_package(&p, &[&fw]);
assert_eq!(
field_type_names(&reduced.decls[0]),
vec!["u32", "nowhere.Missing"]
);
}
#[test]
fn a_doc_comment_does_not_move_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.decls[0].doc = "documented".to_owned();
p.interfaces[0].doc = "documented".to_owned();
assert_eq!(hash_of(&p, &fw), before);
}
#[test]
fn a_nested_doc_comment_does_not_move_the_hash() {
let (mut p, fw) = fixture();
p.decls.push(Decl {
name: "Flags".to_owned(),
kind: Some(decl::Kind::EnumSetDef(EnumSetDef {
bits: vec![EnumValue {
name: "a".to_owned(),
value: 1,
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
..Default::default()
})),
..Default::default()
});
p.interfaces[0].interactions.push(signal("flags", "Flags"));
let before = hash_of(&p, &fw);
p.interfaces[0].interactions[0].doc = "documented".to_owned();
if let Some(decl::Kind::StructDef(def)) = &mut p.decls[0].kind
&& let Some(struct_member::Member::Field(field)) = &mut def.members[0].member
{
field.doc = "documented".to_owned();
}
if let Some(decl::Kind::EnumSetDef(def)) = &mut p.decls[3].kind {
def.bits[0].doc = "documented".to_owned();
}
assert_eq!(hash_of(&p, &fw), before);
}
#[test]
fn a_doc_tag_does_not_move_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.decls[0].labels.push("tagged".to_owned());
p.decls[0].deprecated = Some("use Point2".to_owned());
if let Some(decl::Kind::StructDef(def)) = &mut p.decls[0].kind
&& let Some(struct_member::Member::Field(field)) = &mut def.members[0].member
{
field.labels.push("tagged".to_owned());
field.deprecated = Some("use f9".to_owned());
}
p.interfaces[0].labels.push("tagged".to_owned());
p.interfaces[0].deprecated = Some("use J".to_owned());
p.interfaces[0].interactions[0]
.labels
.push("tagged".to_owned());
p.interfaces[0].interactions[0].deprecated = Some("use pos2".to_owned());
assert_eq!(hash_of(&p, &fw), before);
}
#[test]
fn a_number_moves_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.interfaces[0].number = 7;
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_provisional_flag_moves_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.interfaces[0].provisional = false;
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_retired_entry_does_not_move_the_hash() {
let (mut p, fw) = fixture();
let before = hash_of(&p, &fw);
p.retired.push(RetiredInterface {
name: "Old".to_owned(),
number: 9,
});
assert_eq!(hash_of(&p, &fw), before);
assert!(reduced_package(&p, &[&fw]).retired.is_empty());
}
#[test]
fn an_inline_service_shape_reaches_its_types_and_moves_the_hash() {
let (mut p, fw) = fixture();
p.interfaces.clear();
let before = hash_of(&p, &fw);
p.services
.push(inline_service("p.hvac", vec![signal("temp", "Point")]));
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Coord", "Point", "fw.Unit"]);
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn an_inline_shape_hashes_the_owning_services_visibility() {
let (mut p, fw) = fixture();
p.interfaces.clear();
p.services
.push(inline_service("p.hvac", vec![signal("temp", "Point")]));
let before = hash_of(&p, &fw);
p.services[0].visibility = Visibility::Internal as i32;
assert_ne!(hash_of(&p, &fw), before);
let reduced = reduced_package(&p, &[&fw]);
assert_eq!(reduced.interfaces[0].name, "p.hvac");
assert_eq!(
reduced.interfaces[0].visibility,
Visibility::Internal as i32
);
}
#[test]
fn equal_packages_hash_alike_whatever_the_declaration_order() {
let (mut p, fw) = fixture();
p.interfaces.push(Interface {
name: "J".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 2,
..Default::default()
});
let mut reordered = p.clone();
reordered.decls.reverse();
reordered.interfaces.reverse();
let mut fw_reordered = fw.clone();
fw_reordered.decls.reverse();
assert_ne!(reordered.decls, p.decls);
assert_ne!(reordered.interfaces, p.interfaces);
assert_eq!(
catalog_hash(&reordered, &[&fw_reordered]),
catalog_hash(&p, &[&fw])
);
assert_eq!(
reduced_package(&reordered, &[&fw_reordered]),
reduced_package(&p, &[&fw])
);
}
#[test]
fn the_hashed_package_among_others_is_skipped() {
let p = Package {
name: "p".to_owned(),
decls: vec![
struct_decl("Point", &["Coord", "fw.Unit"]),
scalar_decl("Coord"),
],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 1,
..Default::default()
}],
..Default::default()
};
let fw = Package {
name: "fw".to_owned(),
decls: vec![struct_decl("Unit", &["p.Coord"])],
..Default::default()
};
let reached: Vec<String> = reachable_decls(&p, &[&p, &fw]).into_keys().collect();
assert_eq!(reached, ["Coord", "Point", "fw.Unit"]);
assert_eq!(reduced_package(&p, &[&p, &fw]), reduced_package(&p, &[&fw]));
assert_eq!(catalog_hash(&p, &[&p, &fw]), catalog_hash(&p, &[&fw]));
}
fn const_decl(name: &str, type_ref: &str, value: &str) -> Decl {
Decl {
name: name.to_owned(),
kind: Some(decl::Kind::ConstDef(ConstDef {
type_ref: Some(type_ref.to_owned()),
value: value.to_owned(),
regex: None,
})),
..Default::default()
}
}
fn guarded_command(source: &str) -> Decl {
Decl {
name: "set".to_owned(),
ordinal: 1,
kind: Some(decl::Kind::CommandDef(CommandDef {
params: vec![Param {
name: "level".to_owned(),
r#type: Some(named("Level")),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
contracts: vec![Contract {
kind: ContractKind::Require as i32,
source: source.to_owned(),
..Default::default()
}],
..Default::default()
})),
..Default::default()
}
}
fn guarded_fixture(source: &str) -> Package {
Package {
name: "p".to_owned(),
decls: vec![scalar_decl("Level"), const_decl("MAX", "Level", "100")],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![guarded_command(source)],
number: 1,
..Default::default()
}],
..Default::default()
}
}
fn set_const_value(package: &mut Package, name: &str, value: &str) {
let decl = package.decls.iter_mut().find(|d| d.name == name).unwrap();
let Some(decl::Kind::ConstDef(def)) = &mut decl.kind else {
panic!("{name} is not a constant");
};
def.value = value.to_owned();
}
#[test]
fn a_constant_named_in_a_contract_clause_is_reached() {
let mut p = guarded_fixture("level < MAX");
let reached: Vec<String> = reachable_decls(&p, &[]).into_keys().collect();
assert_eq!(reached, vec!["Level", "MAX"]);
let before = catalog_hash(&p, &[]);
set_const_value(&mut p, "MAX", "200");
assert_ne!(catalog_hash(&p, &[]), before);
}
#[test]
fn a_constant_named_in_a_constant_value_is_reached() {
let mut p = guarded_fixture("level < LIMIT");
p.decls.push(const_decl("LIMIT", "Level", "MAX"));
let reached: Vec<String> = reachable_decls(&p, &[]).into_keys().collect();
assert_eq!(reached, vec!["LIMIT", "Level", "MAX"]);
let before = catalog_hash(&p, &[]);
set_const_value(&mut p, "MAX", "200");
assert_ne!(catalog_hash(&p, &[]), before);
}
fn enum_decl(name: &str) -> Decl {
Decl {
name: name.to_owned(),
kind: Some(decl::Kind::EnumDef(EnumDef::default())),
..Default::default()
}
}
#[test]
fn a_dotted_name_in_an_expression_reaches_its_declarations() {
let mut p = guarded_fixture("mode == Mode.OFF && gear != Gear.PARK");
p.decls.push(enum_decl("Mode"));
let fw = Package {
name: "fw".to_owned(),
decls: vec![enum_decl("Gear")],
..Default::default()
};
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Level", "Mode", "fw.Gear"]);
}
#[test]
fn an_imported_constant_named_in_a_contract_is_reached() {
let p = guarded_fixture("level < LIMIT");
let mut fw = Package {
name: "fw".to_owned(),
decls: vec![const_decl("LIMIT", "Level", "130.0")],
..Default::default()
};
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Level", "fw.LIMIT"]);
let before = hash_of(&p, &fw);
set_const_value(&mut fw, "LIMIT", "140.0");
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_name_in_a_foreign_constant_value_resolves_in_its_own_package() {
let p = guarded_fixture("level < LIMIT");
let fw = Package {
name: "fw".to_owned(),
decls: vec![
const_decl("LIMIT", "Level", "MAX"),
const_decl("MAX", "Level", "7"),
],
..Default::default()
};
let reached: Vec<String> = reachable_decls(&p, &[&fw]).into_keys().collect();
assert_eq!(reached, vec!["Level", "fw.LIMIT", "fw.MAX"]);
}
fn guarded_query(source: &str) -> Decl {
decl_of(
"get",
decl::Kind::QueryDef(QueryDef {
params: vec![param(named("Level"))],
return_type: Some(ReturnType {
kind: Some(return_type::Kind::Value(named("Level"))),
}),
contracts: vec![Contract {
kind: ContractKind::Require as i32,
source: source.to_owned(),
..Default::default()
}],
..Default::default()
}),
)
}
#[test]
fn a_constant_named_in_a_query_contract_is_reached() {
let mut p = guarded_fixture("");
p.interfaces[0].interactions = vec![guarded_query("p < MAX")];
let reached: Vec<String> = reachable_decls(&p, &[]).into_keys().collect();
assert_eq!(reached, vec!["Level", "MAX"]);
let before = catalog_hash(&p, &[]);
set_const_value(&mut p, "MAX", "200");
assert_ne!(catalog_hash(&p, &[]), before);
}
#[test]
fn a_name_in_a_string_literal_or_a_number_is_not_followed() {
let mut p = guarded_fixture(r#"label != "say \"MAX\"" && level > 1e3 && level < 10MAX"#);
p.decls.push(const_decl("e3", "Level", "1"));
let reached: Vec<String> = reachable_decls(&p, &[]).into_keys().collect();
assert_eq!(reached, vec!["Level"]);
}
#[test]
fn an_expression_string_is_hashed_as_written() {
let mut p = guarded_fixture("p.LIMIT");
p.decls.push(const_decl("LIMIT", "Level", "p.FLAG"));
p.decls.push(const_decl("FLAG", "Level", "1"));
let reduced = reduced_package(&p, &[]);
assert!(reduced.decls.iter().any(|d| d.name == "FLAG"));
let Some(decl::Kind::CommandDef(def)) = &reduced.interfaces[0].interactions[0].kind else {
panic!("not a command");
};
assert_eq!(def.contracts[0].source, "p.LIMIT");
let limit = reduced.decls.iter().find(|d| d.name == "LIMIT").unwrap();
let Some(decl::Kind::ConstDef(def)) = &limit.kind else {
panic!("not a constant");
};
assert_eq!(def.value, "p.FLAG");
}
fn field_of(kind: field_type::Kind) -> FieldType {
FieldType {
optional: false,
kind: Some(kind),
}
}
fn holder(ty: FieldType) -> Decl {
Decl {
name: "S".to_owned(),
kind: Some(decl::Kind::StructDef(StructDef {
members: vec![StructMember {
member: Some(struct_member::Member::Field(Box::new(Field {
name: "f".to_owned(),
ordinal: 1,
r#type: Some(ty),
..Default::default()
}))),
}],
fixed_layout: false,
})),
..Default::default()
}
}
fn decl_of(name: &str, kind: decl::Kind) -> Decl {
Decl {
name: name.to_owned(),
ordinal: 1,
kind: Some(kind),
..Default::default()
}
}
fn query(params: Vec<Param>, return_type: Option<return_type::Kind>) -> Decl {
decl_of(
"q",
decl::Kind::QueryDef(QueryDef {
params,
return_type: return_type.map(|kind| ReturnType { kind: Some(kind) }),
..Default::default()
}),
)
}
fn param(ty: FieldType) -> Param {
Param {
name: "p".to_owned(),
r#type: Some(ty),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}
}
fn pattern_const_def(name: &str) -> TypeDef {
TypeDef {
constraint: Some(Constraint {
pattern_const: Some(name.to_owned()),
..Default::default()
}),
..Default::default()
}
}
fn reference_cases() -> Vec<(&'static str, Vec<Decl>, Vec<Decl>)> {
let t = || named("T");
let via_s = || vec![signal("s", "S")];
vec![
("SignalDef.payload", vec![signal("s", "T")], vec![]),
(
"EventDef.payload",
vec![decl_of(
"e",
decl::Kind::EventDef(EventDef {
payload: "T".to_owned(),
..Default::default()
}),
)],
vec![],
),
("FieldType::Named", via_s(), vec![holder(t())]),
(
"UnionArm.type_ref",
via_s(),
vec![decl_of(
"S",
decl::Kind::UnionDef(UnionDef {
arms: vec![UnionArm {
name: "a".to_owned(),
ordinal: 1,
type_ref: "T".to_owned(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
..Default::default()
}),
)],
),
(
"EnumSetDef.backing_enum",
via_s(),
vec![decl_of(
"S",
decl::Kind::EnumSetDef(EnumSetDef {
backing_enum: Some("T".to_owned()),
..Default::default()
}),
)],
),
(
"ConstDef.type_ref",
via_s(),
vec![
decl_of("S", decl::Kind::TypeDef(pattern_const_def("C"))),
decl_of(
"C",
decl::Kind::ConstDef(ConstDef {
type_ref: Some("T".to_owned()),
..Default::default()
}),
),
],
),
(
"Constraint.pattern_const",
via_s(),
vec![decl_of("S", decl::Kind::TypeDef(pattern_const_def("T")))],
),
(
"FieldType::InlineScalar",
via_s(),
vec![holder(field_of(field_type::Kind::InlineScalar(Box::new(
pattern_const_def("T"),
))))],
),
(
"TupleType.fields",
via_s(),
vec![holder(field_of(field_type::Kind::Tuple(TupleType {
fields: vec![TupleField {
name: "x".to_owned(),
r#type: Some(t()),
}],
})))],
),
(
"ArrayType.element",
via_s(),
vec![holder(field_of(field_type::Kind::Array(Box::new(
ArrayType {
element: Some(Box::new(t())),
..Default::default()
},
))))],
),
(
"MapType.key",
via_s(),
vec![holder(field_of(field_type::Kind::Map(Box::new(MapType {
key: Some(Box::new(t())),
..Default::default()
}))))],
),
(
"MapType.value",
via_s(),
vec![holder(field_of(field_type::Kind::Map(Box::new(MapType {
value: Some(Box::new(t())),
..Default::default()
}))))],
),
(
"StreamType::Named",
via_s(),
vec![holder(field_of(field_type::Kind::Stream(StreamType {
element: Some(stream_type::Element::Named("T".to_owned())),
})))],
),
(
"FixedDef.payload",
vec![decl_of(
"k",
decl::Kind::FixedDef(FixedDef { payload: Some(t()) }),
)],
vec![],
),
(
"CommandDef.params",
vec![decl_of(
"c",
decl::Kind::CommandDef(CommandDef {
params: vec![param(t())],
..Default::default()
}),
)],
vec![],
),
(
"QueryDef.params",
vec![query(vec![param(t())], None)],
vec![],
),
(
"ReturnType::Value",
vec![query(vec![], Some(return_type::Kind::Value(t())))],
vec![],
),
(
"FallibleType.ok",
vec![query(
vec![],
Some(return_type::Kind::Fallible(FallibleType {
ok: "T".to_owned(),
err: String::new(),
})),
)],
vec![],
),
(
"FallibleType.err",
vec![query(
vec![],
Some(return_type::Kind::Fallible(FallibleType {
ok: String::new(),
err: "T".to_owned(),
})),
)],
vec![],
),
]
}
#[test]
fn every_reference_kind_reaches_its_declaration_and_moves_the_hash() {
for (kind, interactions, mut decls) in reference_cases() {
decls.push(scalar_decl("T"));
let mut p = Package {
name: "p".to_owned(),
decls,
interfaces: vec![Interface {
name: "I".to_owned(),
interactions,
number: 1,
..Default::default()
}],
..Default::default()
};
assert!(
reachable_decls(&p, &[]).contains_key("T"),
"{kind}: the closure must reach `T`",
);
let before = catalog_hash(&p, &[]);
let target = p.decls.iter_mut().find(|d| d.name == "T").unwrap();
target.kind = Some(decl::Kind::TypeDef(TypeDef {
constraint: Some(Constraint {
min: Some("1".to_owned()),
..Default::default()
}),
..Default::default()
}));
assert_ne!(
catalog_hash(&p, &[]),
before,
"{kind}: a change to `T` must move the hash",
);
}
}
fn two_interface_fixture() -> (Package, Package) {
let (mut p, fw) = fixture();
p.interfaces.push(Interface {
name: "J".to_owned(),
interactions: vec![signal("pos", "Point")],
number: 2,
provisional: true,
..Default::default()
});
(p, fw)
}
#[test]
fn reordering_two_interfaces_does_not_move_the_hash() {
let (mut p, fw) = two_interface_fixture();
let before = hash_of(&p, &fw);
p.interfaces.reverse();
assert_eq!(hash_of(&p, &fw), before);
let names: Vec<String> = reduced_package(&p, &[&fw])
.interfaces
.into_iter()
.map(|i| i.name)
.collect();
assert_eq!(names, ["I", "J"]);
}
fn reduced_interface_names(p: &Package, fw: &Package) -> Vec<String> {
reduced_package(p, &[fw])
.interfaces
.into_iter()
.map(|i| i.name)
.collect()
}
#[test]
fn interfaces_are_ordered_by_number() {
let (mut p, fw) = two_interface_fixture();
p.interfaces[0].number = 2;
p.interfaces[1].number = 1;
assert_eq!(reduced_interface_names(&p, &fw), ["J", "I"]);
}
#[test]
fn interfaces_with_equal_numbers_are_ordered_by_name() {
let (mut p, fw) = two_interface_fixture();
p.interfaces[1].number = 1;
p.interfaces.reverse();
assert_eq!(reduced_interface_names(&p, &fw), ["I", "J"]);
}
#[test]
fn swapping_two_interfaces_numbers_moves_the_hash() {
let (mut p, fw) = two_interface_fixture();
let before = hash_of(&p, &fw);
p.interfaces[0].number = 2;
p.interfaces[1].number = 1;
assert_ne!(hash_of(&p, &fw), before);
}
#[test]
fn a_union_arm_or_enum_value_doc_comment_does_not_move_the_hash() {
let (mut p, fw) = fixture();
p.decls.push(decl_of(
"U",
decl::Kind::UnionDef(UnionDef {
arms: vec![UnionArm {
name: "a".to_owned(),
ordinal: 1,
type_ref: "Coord".to_owned(),
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
..Default::default()
}),
));
p.decls.push(decl_of(
"E",
decl::Kind::EnumDef(EnumDef {
values: vec![EnumValue {
name: "on".to_owned(),
value: 1,
doc: String::new(),
links: Vec::new(),
see: Vec::new(),
since: Vec::new(),
}],
..Default::default()
}),
));
p.interfaces[0].interactions.push(signal("u", "U"));
p.interfaces[0].interactions.push(signal("e", "E"));
let before = hash_of(&p, &fw);
let mut arm_doc = p.clone();
if let Some(decl::Kind::UnionDef(def)) = &mut arm_doc.decls[3].kind {
def.arms[0].doc = "documented".to_owned();
}
assert_ne!(arm_doc, p);
assert_eq!(hash_of(&arm_doc, &fw), before, "a union arm's doc");
let mut value_doc = p.clone();
if let Some(decl::Kind::EnumDef(def)) = &mut value_doc.decls[4].kind {
def.values[0].doc = "documented".to_owned();
}
assert_ne!(value_doc, p);
assert_eq!(hash_of(&value_doc, &fw), before, "an enum value's doc");
}
macro_rules! document {
($carrier:expr $(; $tag:ident = $value:expr)*) => {{
let carrier = &mut $carrier;
carrier.doc = "documented, see [Other]".to_owned();
carrier.links.push(DocLink {
text: "Other".to_owned(),
offset: 16,
len: 7,
target: "fw.Other".to_owned(),
});
carrier.see.push(DocLink {
text: "fw.Other".to_owned(),
target: "fw.Other".to_owned(),
..Default::default()
});
carrier.since.push("1.2".to_owned());
$(carrier.$tag = $value;)*
}};
}
fn every_carrier_fixture() -> Package {
let field = Field {
name: "f".to_owned(),
ordinal: 1,
r#type: Some(named("T")),
..Default::default()
};
let command = decl_of(
"c",
decl::Kind::CommandDef(CommandDef {
params: vec![Param {
name: "x".to_owned(),
r#type: Some(named("E")),
..Default::default()
}],
contracts: vec![Contract {
kind: ContractKind::Require as i32,
source: "x != C".to_owned(),
..Default::default()
}],
..Default::default()
}),
);
let query = decl_of(
"q",
decl::Kind::QueryDef(QueryDef {
params: vec![Param {
name: "y".to_owned(),
r#type: Some(named("F")),
..Default::default()
}],
return_type: Some(ReturnType {
kind: Some(return_type::Kind::Value(named("T"))),
}),
..Default::default()
}),
);
Package {
name: "p".to_owned(),
decls: vec![
scalar_decl("T"),
const_decl("C", "E", "E.ON"),
decl_of(
"S",
decl::Kind::StructDef(StructDef {
members: vec![StructMember {
member: Some(struct_member::Member::Field(Box::new(field))),
}],
fixed_layout: false,
}),
),
decl_of(
"E",
decl::Kind::EnumDef(EnumDef {
values: vec![EnumValue {
name: "ON".to_owned(),
value: 1,
..Default::default()
}],
..Default::default()
}),
),
decl_of(
"F",
decl::Kind::EnumSetDef(EnumSetDef {
bits: vec![EnumValue {
name: "A".to_owned(),
value: 0,
..Default::default()
}],
..Default::default()
}),
),
decl_of(
"U",
decl::Kind::UnionDef(UnionDef {
arms: vec![UnionArm {
name: "s".to_owned(),
ordinal: 1,
type_ref: "S".to_owned(),
..Default::default()
}],
..Default::default()
}),
),
],
interfaces: vec![Interface {
name: "I".to_owned(),
interactions: vec![
signal("s", "S"),
decl_of(
"e",
decl::Kind::EventDef(EventDef {
payload: "U".to_owned(),
..Default::default()
}),
),
command,
query,
decl_of(
"fx",
decl::Kind::FixedDef(FixedDef {
payload: Some(named("T")),
}),
),
],
number: 1,
..Default::default()
}],
services: vec![inline_service("svc", vec![signal("t", "S")])],
..Default::default()
}
}
fn decl_named<'a>(package: &'a mut Package, name: &str) -> &'a mut Decl {
package.decls.iter_mut().find(|d| d.name == name).unwrap()
}
fn interaction_named<'a>(package: &'a mut Package, name: &str) -> &'a mut Decl {
package.interfaces[0]
.interactions
.iter_mut()
.find(|d| d.name == name)
.unwrap()
}
fn inline_shape_of(package: &mut Package) -> &mut Interface {
match &mut package.services[0].shapes[0].kind {
Some(service_shape::Kind::Inline(interface)) => interface,
_ => panic!("svc has no inline shape"),
}
}
type SetDocs<'a> = Box<dyn Fn(&mut Package) + 'a>;
#[test]
fn catalog_hash_ignores_every_doc_field() {
let p = every_carrier_fixture();
let reached: Vec<String> = reachable_decls(&p, &[]).into_keys().collect();
assert_eq!(reached, vec!["C", "E", "F", "S", "T", "U"]);
let before = catalog_hash(&p, &[]);
let labels = vec!["tagged".to_owned()];
let deprecated = Some("use another".to_owned());
let cases: Vec<(&str, SetDocs<'_>)> = vec![
(
"a declaration",
Box::new(
|p| document!(*decl_named(p, "T"); labels = labels.clone(); deprecated = deprecated.clone()),
),
),
(
"a struct field",
Box::new(|p| {
if let Some(decl::Kind::StructDef(def)) = &mut decl_named(p, "S").kind
&& let Some(struct_member::Member::Field(field)) =
&mut def.members[0].member
{
document!(*field; labels = labels.clone(); deprecated = deprecated.clone());
}
}),
),
(
"an enum value",
Box::new(|p| {
if let Some(decl::Kind::EnumDef(def)) = &mut decl_named(p, "E").kind {
document!(def.values[0]);
}
}),
),
(
"an enum set bit",
Box::new(|p| {
if let Some(decl::Kind::EnumSetDef(def)) = &mut decl_named(p, "F").kind {
document!(def.bits[0]);
}
}),
),
(
"a union arm",
Box::new(|p| {
if let Some(decl::Kind::UnionDef(def)) = &mut decl_named(p, "U").kind {
document!(def.arms[0]);
}
}),
),
(
"an interface",
Box::new(
|p| document!(p.interfaces[0]; labels = labels.clone(); deprecated = deprecated.clone()),
),
),
(
"an interaction",
Box::new(
|p| document!(*interaction_named(p, "s"); labels = labels.clone(); deprecated = deprecated.clone()),
),
),
(
"a command parameter",
Box::new(|p| {
if let Some(decl::Kind::CommandDef(def)) = &mut interaction_named(p, "c").kind {
document!(def.params[0]);
}
}),
),
(
"a query parameter",
Box::new(|p| {
if let Some(decl::Kind::QueryDef(def)) = &mut interaction_named(p, "q").kind {
document!(def.params[0]);
}
}),
),
(
"a service",
Box::new(
|p| document!(p.services[0]; labels = labels.clone(); deprecated = deprecated.clone()),
),
),
(
"an inline shape",
Box::new(
|p| document!(*inline_shape_of(p); labels = labels.clone(); deprecated = deprecated.clone()),
),
),
(
"an inline shape's interaction",
Box::new(
|p| document!(inline_shape_of(p).interactions[0]; labels = labels.clone(); deprecated = deprecated.clone()),
),
),
];
for (carrier, set_docs) in &cases {
let mut documented = p.clone();
set_docs(&mut documented);
assert_ne!(documented, p, "{carrier}: the case must change the IR");
assert_eq!(catalog_hash(&documented, &[]), before, "{carrier}");
}
}
}